"""DNS Authenticator for ISPmanager."""
import json
import logging
import time

import requests
import urllib3
import zope.interface

from certbot import errors
from certbot import interfaces
from certbot.plugins import dns_common

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

logger = logging.getLogger(__name__)


@zope.interface.implementer(interfaces.IAuthenticator)
@zope.interface.provider(interfaces.IPluginFactory)
class Authenticator(dns_common.DNSAuthenticator):
    """DNS Authenticator for ISPmanager

    This Authenticator uses the ISPmanager API to fulfill a dns-01 challenge.
    """

    description = "Obtain certificates using a DNS TXT record (if you are using ISPmanager for DNS)."
    ttl = 60

    def __init__(self, *args, **kwargs):
        super(Authenticator, self).__init__(*args, **kwargs)
        self.credentials = None

    @classmethod
    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ
        super(Authenticator, cls).add_parser_arguments(
            add, default_propagation_seconds=120
        )
        add("credentials", help="ISPmanager API credentials INI file.")
        add("username", help="Username for ISPmanager API (can be used instead of INI file).")
        add("password", help="Password for ISPmanager API (can be used instead of INI file).")
        add("endpoint", help="URL of the ISPmanager API (can be used instead of INI file).")

    def more_info(self):  # pylint: disable=missing-docstring,no-self-use
        return (
            "This plugin configures a DNS TXT record to respond to a dns-01 challenge using "
            + "the ISPmanager API."
        )

    def _setup_credentials(self):
        if self.conf("username") and self.conf("password") and self.conf("endpoint"):
            self.credentials = None
        else:
            self.credentials = self._configure_credentials(
                "credentials",
                "ISPmanager credentials INI file",
                {
                    "endpoint": "URL of the ISPmanager API (e.g., https://example.com:1500/ispmgr).",
                    "username": "Username for ISPmanager API.",
                    "password": "Password for ISPmanager API.",
                },
            )

    def _perform(self, domain, validation_name, validation):
        self._get_ispmgr_client(domain).add_txt_record(
            validation_name, validation
        )

    def _cleanup(self, domain, validation_name, validation):
        self._get_ispmgr_client(domain).del_txt_record(
            validation_name, validation
        )

    def _get_ispmgr_client(self, domain):
        if self.credentials is None:
            endpoint = self.conf("endpoint")
            username = self.conf("username")
            password = self.conf("password")
        else:
            endpoint = self.credentials.conf("endpoint")
            username = self.credentials.conf("username")
            password = self.credentials.conf("password")
            
        return _ISPmanagerClient(
            endpoint,
            username,
            password,
            domain
        )


class _ISPmanagerClient(object):
    """
    Encapsulates all communication with the ISPmanager API.
    """

    def __init__(self, endpoint, username, password, domain):
        logger.debug("Creating ISPmanagerClient")
        self.endpoint = endpoint.rstrip("/")
        if not self.endpoint.endswith("ispmgr"):
            self.endpoint += "/ispmgr"
        self.username = username
        self.password = password
        self.domain = domain
        self.session = requests.Session()
        self.session_id = None

    def _get_main_domain(self, domain):
        """
        Extract the main domain from a subdomain/domain string.
        """
        domain = domain.rstrip('.')
        parts = domain.split('.')
        if len(parts) > 2:
            return parts[-2] + '.' + parts[-1]
        return domain

    def _login(self):
        if self.session_id is not None:
            return
        logger.debug("Logging in to ISPmanager API")
        
        params = {
            "func": "auth",
            "username": self.username,
            "password": self.password,
            "out": "json"
        }
        
        try:
            resp = self.session.get(self.endpoint, params=params, verify=False)
            resp.raise_for_status()
            result = resp.json()
        except requests.exceptions.RequestException as e:
            raise errors.PluginError(f"HTTP Error during login: {e}")
        except json.decoder.JSONDecodeError:
            raise errors.PluginError(f"API response with non JSON: {resp.text}")

        if "auth" in result:
            self.session_id = result["auth"]
            logger.debug("Session ID obtained successfully")
        else:
            raise errors.PluginError(f"Failed to authenticate with ISPmanager API. Result: {result}")

    def _api_request(self, params):
        if self.session_id is None:
            self._login()

        request_params = {"auth": self.session_id, "out": "json"}
        request_params.update(params)

        try:
            resp = self.session.get(self.endpoint, params=request_params, verify=False)
            logger.debug(f"API Request to {self.endpoint} with params: {params}")
            resp.raise_for_status()
            result = resp.json()
        except requests.exceptions.RequestException as e:
            raise errors.PluginError(f"HTTP Error during API request: {e}")
        except json.decoder.JSONDecodeError:
            raise errors.PluginError(f"API response with non JSON: {resp.text}")

        time.sleep(1) # Similar delay to PHP implementation to avoid issues
        return result

    def get_existing_txt(self, main_domain):
        """
        Get all DNS TXT records.
        """
        params = {
            "func": "domain.sublist",
            "elid": main_domain
        }
        data = self._api_request(params)
        
        records = []
        if "elem" in data:
            for record in data["elem"]:
                if "_acme-challenge" in record.get("name", "") and record.get("type", "") == "TXT":
                    records.append(record.get("key"))
        return records

    def add_txt_record(self, record_name, record_content):
        """
        Add a TXT record.
        """
        main_domain = self._get_main_domain(self.domain)
        # Ensure name is FQDN ending with dot as seen in PHP example '_acme-challenge.domain.'
        record_name_fqdn = record_name if record_name.endswith('.') else record_name + '.'
        
        params = {
            "func": "domain.sublist.edit",
            "plid": main_domain,
            "name": record_name_fqdn,
            "sdtype": "TXT",
            "addr": record_content,
            "sok": "ok"
        }
        logger.debug(f"Adding TXT record {record_name_fqdn} to {main_domain}")
        data = self._api_request(params)
        
        if data.get("result") == "OK":
            logger.info(f"Successfully added TXT record for {record_name_fqdn}")
        else:
            error_msg = data.get("error", "Unknown error")
            raise errors.PluginError(f"Failed to add TXT record: {error_msg}")

    def del_txt_record(self, record_name, record_content):
        """
        Delete a TXT record.
        """
        main_domain = self._get_main_domain(self.domain)
        records = self.get_existing_txt(main_domain)
        
        # We delete all records matching the acme challenge name 
        # (the keys often encode the name and type, such as '_acme-challenge.example.com. TXT')
        record_name_match = record_name if record_name.endswith('.') else record_name + '.'
        deleted_count = 0
        
        for key in records:
            if key and record_name_match in key:
                params = {
                    "func": "domain.sublist.delete",
                    "plid": main_domain,
                    "elid": key
                }
                logger.debug(f"Deleting TXT record with key '{key}' from {main_domain}")
                data = self._api_request(params)
                if data.get("result") == "OK":
                    logger.info(f"Successfully deleted TXT record '{key}'")
                    deleted_count += 1
                else:
                    logger.warning(f"Failed to delete TXT record '{key}': {data}")
                    
        if deleted_count == 0:
            logger.debug(f"No existing TXT records found to delete for {record_name_match}")
