# 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 <http://www.gnu.org/licenses/>.
#
# Copyright 2024 SerialLab Corp.  All rights reserved.
import os
import uuid
import time
import sqlite3
import requests
from lxml import etree
from urllib.parse import urlparse
from django.utils.translation import gettext as _
from requests.auth import HTTPBasicAuth, HTTPProxyAuth
from quartet_capture.rules import RuleContext
from quartet_output import errors
from quartet_output.transport.http import HttpTransportMixin, user_agent
from quartet_output.models import EndPoint
from quartet_capture import models, rules, errors as capture_errors
from list_based_flavorpack.models import ListBasedRegion
from list_based_flavorpack.processing_classes.third_party_processing.rules import (
    get_region_table,
)


class NumberRequestTransportStep(rules.Step, HttpTransportMixin):
    """
    Uses the transport information within the `region`.
    """

    def execute(self, data, rule_context: RuleContext):
        # get the task parameters that we rely on
        try:
            self.info(
                _(
                    "Looking for the task parameter with the target Region. "
                    "Output Name."
                )
            )
            param = models.TaskParameter.objects.get(
                task__name=rule_context.task_name, name="List-based Region"
            )
            # now see if we can get the output critieria based on the param
            # value
            self.info(
                _(
                    "Found the region param, now looking up the "
                    "Region instance with name %s."
                ),
                param.value,
            )
        except models.TaskParameter.DoesNotExist:
            raise capture_errors.ExpectedTaskParameterError(
                _(
                    "The task parameter with name List-based Region "
                    "could not be found.  This task parameter is required by "
                    "the NumberRequestTransportStep to function correctly."
                )
            )
        try:
            region = ListBasedRegion.objects.get(machine_name=param.value)
            # check the url/urn to see if we support the protocol
            protocol = self._supports_protocol(region.end_point)
            self.info(
                "Protocol supported.  Sending message to %s." % region.end_point.urn
            )
            response = self._send_message(data, protocol, rule_context, region)
            if "ErrorCode" in response.content.decode("utf-8"):
                raise self.ResponseError(response.content.decode("utf-8"))
            # Pass response for downstream processing.
            rule_context.context["NUMBER_RESPONSE"] = response.content
        except Exception as e:
            self.error(
                _("An error occurred while sending request to third party: %s"), str(e)
            )
            raise

    class ResponseError(Exception):
        pass

    def get_auth(self, region):
        """
        Get's the authentication method and credentials from the
        region record.
        :param region: A ListBasedModel model instance.
        :return: A `requests.auth.HTTPBasicAuth` or `HTTPProxyAuth`
        """
        auth_info = region.authentication_info
        auth = None
        if auth_info:
            auth_type = auth_info.type or ""
            if "digest" in auth_type.lower():
                auth = HTTPBasicAuth
            elif "proxy" in auth_type.lower():
                auth = HTTPProxyAuth
            else:
                auth = HTTPBasicAuth
            auth = auth(auth_info.username, auth_info.password)
        return auth

    def post_data(
        self,
        data: str,
        rule_context: RuleContext,
        region: ListBasedRegion,
        content_type="application/xml",
        file_extension="xml",
        http_put=False,
    ):
        """
        :param data_context_key: The key within the rule_context that contains
         the data to post.  If being invoked from the internals of this
         module this is usually the OUTBOUND_EPCIS_MESSSAGE_KEY value of the
         `quartet_output.steps.ContextKeys` Enum.
        :param output_criteria: The output criteria containing the connection
        info.
        :return: The response.
        """
        if not http_put:
            func = requests.post
        else:
            func = requests.put
        response = func(
            region.end_point.urn,
            data,
            auth=self.get_auth(region),
            headers={"content-type": content_type, "user-agent": user_agent},
        )
        return response

    def _send_message(
        self,
        data: str,
        protocol: str,
        rule_context: RuleContext,
        region: ListBasedRegion,
    ):
        """
        Sends a message using the protocol specified.
        :param protocol: The scheme of the urn in the output_criteria endpoint.
        :param rule_context: The RuleContext contains the data in the
        OUTBOUND_EPCIS_MESSAGE_KEY value from the `ContextKey` class.
        :param region: The originating region.
        :return: None.
        """
        content_type = self.get_parameter("content-type", "application/xml")
        file_extension = self.get_parameter("file-extension", "xml")
        put_data = self.get_boolean_parameter("put-data")
        if protocol.lower() in ["http", "https"]:
            if not put_data:
                func = self.post_data
            else:
                func = self.put_data
            self.info("request sent %s", data)
            response = func(data, rule_context, region, content_type, file_extension)
            try:
                response.raise_for_status()
            except:
                if response.content:
                    self.info(
                        "Error occurred with following response %s", response.content
                    )
                raise
            self.info("Response Received %s", response.content)
            return response

    def _supports_protocol(self, endpoint: EndPoint):
        """
        Inspects the output settings and determines if this step can support
        the protocol or not. Override this to support another or more
        protocols.
        :param EndPoint: the endpoint to inspect
        :return: Returns the supported scheme if the protocol is supported or
        None.
        """
        parse_result = urlparse(endpoint.urn)
        if parse_result.scheme.lower() in ["http", "https"]:
            return parse_result.scheme
        else:
            raise errors.ProtocolNotSupportedError(
                _(
                    "The protocol specified in urn %s is not supported by this "
                    "step or module."
                ),
                endpoint.urn,
            )

    def on_failure(self):
        super().on_failure()

    @property
    def declared_parameters(self):
        return {
            "content-type": "The content-type to add to the header during any "
            "http posts, puts, etc. Default is application/"
            "xml",
            "file-extension": "The file extension to specify when posting and "
            "putting data via http. Default is xml",
        }


class UUIDRequestStep(rules.Step):
    """
    Returns UUIDs.
    Excepts a request data as follows:
    <?xml version="1.0" encoding="UTF-8"?>
    <NumberRequest>
        <Type>UUID</Type>
        <Size>10</Size>
    </NumberRequest>
    """

    def execute(self, data, rule_context: RuleContext):
        # get the task parameters that we rely on
        try:
            self.info(
                _(
                    "Looking for the task parameter with the target Region. "
                    "Output Name."
                )
            )
            param = models.TaskParameter.objects.get(
                task__name=rule_context.task_name, name="List-based Region"
            )
            # now see if we can get the output critieria based on the param
            # value
            self.info(
                _(
                    "Found the region param, now looking up the "
                    "Region instance with name %s."
                ),
                param.value,
            )
        except models.TaskParameter.DoesNotExist:
            raise capture_errors.ExpectedTaskParameterError(
                _(
                    "The task parameter with name List-based Region "
                    "could not be found.  This task parameter is required by "
                    "the NumberRequestTransportStep to function correctly."
                )
            )
        try:
            region = ListBasedRegion.objects.get(machine_name=param.value)
            # check the url/urn to see if we support the protocol
            root = etree.fromstring(data)
            size = int(root.find(".//Size").text)
            self.persist_data(region, size)
        except Exception as e:
            self.info(
                _("An error occurred while sending request to third party: %s"), str(e)
            )
            self.info(_("Inbound data %s"), str(data))
            raise

    def persist_data(self, region, size):
        """
        Saves the data to file.  Override to save it other places.
        :param region: The region to save the data for.
        :param size: The number of UUIDs to generate.
        :return: None
        """
        with open(region.file_path, "a") as f:
            for i in range(size):
                f.write("%s\n" % uuid.uuid1())

    def on_failure(self):
        super().on_failure()

    @property
    def declared_parameters(self):
        return {
            "content-type": "The content-type to add to the header during any "
            "http posts, puts, etc. Default is application/"
            "xml",
            "file-extension": "The file extension to specify when posting and "
            "putting data via http. Default is xml",
        }


class UUIDRequestDBStep(UUIDRequestStep):
    """
    Instead of saving data to file will save to a sqlite3 database file.
    """

    def persist_data(self, region, size):
        start = time.time()
        connection = sqlite3.connect(region.db_file_path)
        cursor = connection.cursor()
        cursor.execute("begin transaction")
        self.info("storing the numbers.")
        for i in range(size):
            cursor.execute(
                "insert into %s (serial_number, used) "
                "values (?, ?)" % get_region_table(region),
                (str(uuid.uuid1()), 0),
            )
        cursor.execute("commit")
        self.info("Execution time: %.3f seconds." % (time.time() - start))
