"""Organization related plugins."""

import logging
import re

from django.conf import settings
from django.utils.text import slugify

import requests
from requests.adapters import HTTPAdapter, Retry

from mailbox_manager.enums import MailDomainRoleChoices
from mailbox_manager.models import MailDomain, MailDomainAccess

logger = logging.getLogger(__name__)


class ApiCall:
    """Encapsulates a call to an external API"""

    inputs: dict = {}
    method: str = "GET"
    base: str = ""
    url: str = ""
    params: dict = {}
    headers: dict = {}
    response_data = None

    def execute(self):
        """Call the specified API endpoint with supplied parameters and record response"""
        if self.method in ("POST", "PATCH"):
            response = requests.request(
                method=self.method,
                url=f"{self.base}/{self.url}",
                json=self.params,
                headers=self.headers,
                timeout=20,
            )
        else:
            response = requests.request(
                method=self.method,
                url=f"{self.base}/{self.url}",
                params=self.params,
                headers=self.headers,
                timeout=20,
            )
        self.response_data = response.json()
        logger.info(
            "API call: %s %s %s %s",
            self.method,
            self.url,
            self.params,
            self.response_data,
        )


class CommuneCreation:
    """
    This plugin handles setup tasks for French communes.
    """

    _api_url = "https://recherche-entreprises.api.gouv.fr/search?q={siret}"

    def get_organization_name_from_results(self, data, siret):
        """Return the organization name from the results of a SIRET search."""
        for result in data["results"]:
            nature = "nature_juridique"
            commune = nature in result and result[nature] == "7210"
            if commune:
                return result["siege"]["libelle_commune"].title()

        logger.warning("Not a commune: SIRET %s", siret)
        return None

    def dns_call(self, spec):
        """Call to add a DNS record"""
        zone_name = self.zone_name(spec.inputs["name"])

        records = [
            {
                "name": item["target"],
                "type": item["type"].upper(),
                "data": item["value"],
                "ttl": 3600,
            }
            for item in spec.response_data
        ]
        result = ApiCall()
        result.method = "PATCH"
        result.base = "https://api.scaleway.com"
        result.url = f"/domain/v2beta1/dns-zones/{zone_name}/records"
        result.params = {"changes": [{"add": {"records": records}}]}
        result.headers = {"X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS}
        return result

    def normalize_name(self, name: str) -> str:
        """Map the name to a standard form"""
        name = re.sub("'", "-", name)
        return slugify(name)

    def zone_name(self, name: str) -> str:
        """Derive the zone name from the commune name"""
        normalized = self.normalize_name(name)
        return f"{normalized}.{settings.DNS_PROVISIONING_TARGET_ZONE}"

    def complete_commune_creation(self, name: str) -> ApiCall:
        """Specify the tasks to be completed after a commune is created."""
        inputs = {"name": self.normalize_name(name)}

        create_zone = ApiCall()
        create_zone.method = "POST"
        create_zone.base = "https://api.scaleway.com"
        create_zone.url = "/domain/v2beta1/dns-zones"
        create_zone.params = {
            "project_id": settings.DNS_PROVISIONING_RESOURCE_ID,
            "domain": settings.DNS_PROVISIONING_TARGET_ZONE,
            "subdomain": inputs["name"],
        }
        create_zone.headers = {
            "X-Auth-Token": settings.DNS_PROVISIONING_API_CREDENTIALS
        }

        zone_name = self.zone_name(inputs["name"])

        create_domain = ApiCall()
        create_domain.method = "POST"
        create_domain.base = settings.MAIL_PROVISIONING_API_URL
        create_domain.url = "/domains/"
        create_domain.params = {
            "name": zone_name,
            "delivery": "virtual",
            "features": ["webmail", "mailbox"],
            "context_name": zone_name,
        }
        create_domain.headers = {
            "Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
        }

        spec_domain = ApiCall()
        spec_domain.inputs = inputs
        spec_domain.base = settings.MAIL_PROVISIONING_API_URL
        spec_domain.url = f"/domains/{zone_name}/spec"
        spec_domain.headers = {
            "Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
        }

        return [create_zone, create_domain, spec_domain]

    def complete_zone_creation(self, spec_call):
        """Specify the tasks to be performed to set up the zone."""
        return self.dns_call(spec_call)

    def run_after_create(self, organization):
        """After creating an organization, update the organization name."""
        logger.info("In CommuneCreation")
        if not organization.registration_id_list:
            # No registration ID to convert...
            return

        # In the nominal case, there is only one registration ID because
        # the organization has been created from it.
        try:
            # Retry logic as the API may be rate limited
            s = requests.Session()
            retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[429])
            s.mount("https://", HTTPAdapter(max_retries=retries))

            siret = organization.registration_id_list[0]
            response = s.get(self._api_url.format(siret=siret), timeout=10)
            response.raise_for_status()
            data = response.json()
            name = self.get_organization_name_from_results(data, siret)
            # Not a commune ?
            if not name:
                return
        except requests.RequestException as exc:
            logger.exception("%s: Unable to fetch organization name from SIRET", exc)
            return

        organization.name = name
        organization.save(update_fields=["name", "updated_at"])
        logger.info("Organization %s name updated to %s", organization, name)

        zone_name = self.zone_name(name)
        support = "support-regie@numerique.gouv.fr"
        MailDomain.objects.get_or_create(name=zone_name, support_email=support)

        # Compute and execute the rest of the process
        tasks = self.complete_commune_creation(name)
        for task in tasks:
            task.execute()
        last_task = self.complete_zone_creation(tasks[-1])
        last_task.execute()

    def complete_grant_access(self, sub, zone_name):
        """Specify the tasks to be completed after making a user admin"""
        create_user = ApiCall()
        create_user.method = "POST"
        create_user.base = settings.MAIL_PROVISIONING_API_URL
        create_user.url = "/users/"
        create_user.params = {
            "name": sub,
            "password": "no",
            "is_admin": False,
            "perms": [],
        }
        create_user.headers = {
            "Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
        }

        grant_access = ApiCall()
        grant_access.method = "POST"
        grant_access.base = settings.MAIL_PROVISIONING_API_URL
        grant_access.url = "/allows/"
        grant_access.params = {
            "user": sub,
            "domain": zone_name,
        }
        grant_access.headers = {
            "Authorization": f"Basic {settings.MAIL_PROVISIONING_API_CREDENTIALS}"
        }

        return [create_user, grant_access]

    def run_after_grant_access(self, organization_access):
        """After granting an organization access, check for needed domain access grant."""
        orga = organization_access.organization
        user = organization_access.user
        zone_name = self.zone_name(orga.name)

        try:
            domain = MailDomain.objects.get(name=zone_name)
        except MailDomain.DoesNotExist:
            domain = None

        if domain:
            MailDomainAccess.objects.create(
                domain=domain, user=user, role=MailDomainRoleChoices.OWNER
            )

            tasks = self.complete_grant_access(user.sub, zone_name)
            for task in tasks:
                task.execute()
