# BFH Science Self-Service Portal
# Copyright (C) 2025-2026 IKUS Software
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import logging
from collections import namedtuple
from urllib.parse import quote_plus, urlsplit

import cherrypy
import requests
from cherrypy.process.plugins import SimplePlugin
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

import bfh_science_ssp

logger = logging.getLogger(__name__)

TicketCreateResponse = namedtuple('TicketCreateResponse', 'ticket_id,ticket_number,ticket_url')


class ZnunyPlugin(SimplePlugin):
    """
    Plugin to query Issue Tracking System (Znuny).

    The Znuny REST api is hightly configurable allowing the web service name and the operation endpoint to be configurable. Make sure to validate each endpoint.

    e.g.:
    ```
    https://example.com/otrs/nph-genericinterface.pl/Webservice/<webservice-name>/<ticket-create-endpoint>
    https://servicedesk.bfh.ch/otrs/nph-genericinterface.pl/Webservice/lni-ssp/TicketCreate
    ```

    This is all configuration in the Administration view.
    """

    userlogin = None
    password = None
    ticket_create_url = None

    _session = None

    def start(self):
        self.bus.log('Start Znuny plugin')
        if not self.ticket_create_url:
            return
        if not self.userlogin:
            raise ValueError('userlogin is required for Znuny plugin')
        if not self.password:
            raise ValueError('password is required for Znuny plugin')
        # Configure HTTP connection pool.
        self._session = requests.Session()
        # Define a user agent.
        self._session.headers.update(
            {
                'User-Agent': f'BFH-SSP/{bfh_science_ssp.__version__}',
            }
        )
        # Follow HTTP Redirect
        self._session.allow_redirects = True
        # Add retry backoff logic
        retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504])
        adapter = HTTPAdapter(max_retries=retries, pool_connections=5, pool_maxsize=25)
        self._session.mount("http://", adapter)
        self._session.mount("https://", adapter)

    def stop(self):
        self.bus.log('Stop Znuny plugin')
        if self._session:
            self._session.close()

    def create_ticket(
        self,
        title,
        queue=None,
        lock=None,
        service=None,
        sla=None,
        state=None,
        priority=None,
        type=None,
        customer_user=None,
        owner=None,
        responsible=None,
        communication_channel=None,  # e.g., 'Email', 'Internal', 'Phone'
        is_visible_for_customer=1,  # 0/1
        sender_type="agent",  # 'agent' | 'system' | 'customer'
        auto_response_type=None,  # e.g., 'auto reply', 'auto follow up', ...
        _from=None,
        to=None,
        cc=None,
        bcc=None,
        subject=None,
        body=None,
        mime_type=None,  # e.g. 'text/html'
        charset=None,  # e.g. 'utf-8'
        history_type=None,  # e.g., 'NewTicket', 'AddNote', 'EmailAgent'
        history_comment=None,
        time_unit=None,  # numeric
        no_agent_notify=None,  # 0 or 1
    ):
        """
        Create a ticket via the Znuny GenericInterface TicketCreate operation.

        This method composes a payload with Ticket and Article data according to
        Znuny's TicketCreate contract and sends it to the configured
        `ticket_create_operation_url`. On success, it returns the numeric
        TicketID of the newly created ticket.

        Args:
            title (str): Ticket title. Used also as article subject if `subject` is not given.
            queue (str | None): Queue name. Either Queue or QueueID must exist in your system.
            lock (str | None): Ticket lock state (e.g., "unlock", "lock").
            service (str | None): Service name.
            sla (str | None): SLA name.
            state (str | None): Ticket state (e.g., "new", "open", "closed successful").
            priority (str | None): Ticket priority (e.g., "3 normal").
            type (str | None): Ticket type (e.g., "Incident", "ServiceRequest").
            customer_user (str | None): Customer user login (e.g., "jsmith").
            owner (str | None): Owner user login.
            responsible (str | None): Responsible user login.
            communication_channel (str | None): Optional channel indicator (e.g., "Email", "Phone", "Internal").
            is_visible_for_customer (int | bool): 1/True to show the article in the customer portal; 0/False to hide.
            sender_type (str): Article sender type. Typically "agent". Other values: "system", "customer".
            auto_response_type (str | None): Auto response type for the article (e.g., "auto reply").
            _from (str | None): Email From header (for email-like articles).
            to (str | None): Email To header.
            cc (str | None): Email Cc header.
            bcc (str | None): Email Bcc header.
            subject (str | None): Article subject. Defaults to `title` if omitted.
            body (str | None): Article body. Defaults to empty string if omitted.
            mime_type (str | None): MIME type part (e.g., "text/plain" or "text/html").
            charset (str | None): Charset part (e.g., "utf-8").
            history_type (str | None): History type for the article (e.g., "NewTicket", "AddNote", "EmailAgent").
            history_comment (str | None): History comment text.
            time_unit (int | float | None): Time accounting in minutes for this article.
            no_agent_notify (int | bool | None): 1/True to suppress agent notifications; 0/False to allow.

        Returns:
            int | None: The newly created TicketID on success, or None on failure or if
            the response does not contain a TicketID.

        Raises:
            This method catches all exceptions and returns None. For debugging, consider
            removing the broad exception handler or logging the exception details.

        Notes:
            - Znuny requires a Ticket object (with at least Title and typically Queue/State/Priority)
            and an Article object (with at least Body, Subject, SenderType, ContentType).
            - If you need to use IDs instead of names (e.g., QueueID, StateID, TypeID),
            extend the payload mapping accordingly.
            - Attachments and dynamic fields are not included here; they can be added to
            the Article (Attachment[]) and Ticket (DynamicField[]) per Znuny docs.

        Example:
            response = self.create_ticket(
                title="Unable to access VPN",
                queue="IT::Support",
                state="new",
                priority="3 normal",
                type="Incident",
                customer_user="jdoe",
                service="Network",
                subject="VPN login fails",
                body="<p>User cannot log in to VPN</p>",
                sender_type="agent",
                is_visible_for_customer=1,
            )
            print(f"Created ticket #{response.ticket_id}")
        """
        if not self.ticket_create_url:
            raise RuntimeError('Znuny ITS is not configured')
        # Build Ticket object from supplied values
        ticket_pairs = (
            ("Title", title),
            ("Queue", queue),
            ("Lock", lock),
            ("Service", service),
            ("SLA", sla),
            ("State", state),
            ("Priority", priority),
            ("Type", type),
            ("CustomerUser", customer_user),
            ("Owner", owner),
            ("Responsible", responsible),
        )
        ticket_obj = {field: value for field, value in ticket_pairs if value is not None}

        # ContentType rules: either a ready ContentType OR MimeType+Charset
        # If user gave neither, default to text/plain UTF-8 to be safe.
        content_type = "text/plain; charset=utf-8"
        if mime_type and charset:
            content_type = f"{mime_type}; charset={charset}"
        elif mime_type and not charset:
            content_type = f"{mime_type}; charset=utf-8"
        elif not mime_type and charset:
            content_type = f"text/plain; charset={charset}"

        # Build Article object
        article_pairs = (
            ("Subject", subject or title),
            ("SenderType", sender_type),
            (
                "IsVisibleForCustomer",
                (
                    int(is_visible_for_customer)
                    if isinstance(is_visible_for_customer, bool)
                    else (is_visible_for_customer or 0)
                ),
            ),
            ("CommunicationChannel", communication_channel),
            ("From", _from),
            ("To", to),
            ("Cc", cc),
            ("Bcc", bcc),
            ("AutoResponseType", auto_response_type),
            ("HistoryType", history_type),
            ("HistoryComment", history_comment),
            ("TimeUnit", time_unit),
            ("NoAgentNotify", int(no_agent_notify) if no_agent_notify is not None else None),
            ("Body", body or ""),
            ("ContentType", content_type),
        )
        article_obj = {field: value for field, value in article_pairs if value is not None}

        # Build final payload
        payload = {
            "UserLogin": self.userlogin,
            "Password": self.password,
            "Ticket": ticket_obj,
            "Article": article_obj,
        }

        headers = {"Content-Type": "application/json"}
        resp = self._session.post(self.ticket_create_url, json=payload, headers=headers, timeout=30)
        resp.raise_for_status()
        data = resp.json()

        # Znuny typically returns { TicketID, TicketNumber, ArticleID, ... } on success.
        ticket_id = data.get("TicketID")
        ticket_number = data.get("TicketNumber")
        base_url = urlsplit(self.ticket_create_url)
        ticket_url = f"{base_url.scheme}://{base_url.netloc}/otrs/index.pl?Action=AgentTicketZoom;TicketID={quote_plus(ticket_id)}"
        return TicketCreateResponse(ticket_id, ticket_number, ticket_url)


cherrypy.znuny = ZnunyPlugin(cherrypy.engine)
cherrypy.znuny.subscribe()

cherrypy.config.namespaces['znuny'] = lambda key, value: setattr(cherrypy.znuny, key, value)
