import os
import shlex
import subprocess
import tempfile
import time
from datetime import datetime

import jingrow
import OpenSSL
from jingrow.model.page import Page

from jcloud.api.site import check_dns_cname_a
from jcloud.overrides import get_permission_query_conditions_for_pagetype
from jcloud.runner import Ansible
from jcloud.utils import get_current_team, log_error


class TLSCertificate(Page):
    # begin: auto-generated types
    # This code is auto-generated. Do not modify anything in this block.

    from typing import TYPE_CHECKING

    if TYPE_CHECKING:
        from jingrow.types import DF

        certificate: DF.Code | None
        decoded_certificate: DF.Code | None
        domain: DF.Data
        error: DF.Code | None
        expires_on: DF.Datetime | None
        full_chain: DF.Code | None
        intermediate_chain: DF.Code | None
        issued_on: DF.Datetime | None
        private_key: DF.Code | None
        retry_count: DF.Int
        rsa_key_size: DF.Literal["2048", "3072", "4096"]
        status: DF.Literal["Pending", "Active", "Expired", "Revoked", "Failure"]
        team: DF.Link | None
        wildcard: DF.Check
    # end: auto-generated types

    def autoname(self):
        if self.wildcard:
            self.name = f"*.{self.domain}"
        else:
            self.name = self.domain

    def after_insert(self):
        self.obtain_certificate()

    def on_update(self):
        if self.is_new():
            return
        if self.has_value_changed("rsa_key_size"):
            self.obtain_certificate()

    @jingrow.whitelist()
    def obtain_certificate(self):
        """
        触发自动或手动签发证书时，排队执行 _obtain_certificate。
        """
        (user, session_data, team) = (
            jingrow.session.user,
            jingrow.session.data,
            get_current_team(),
        )
        jingrow.set_user(jingrow.get_value("Team", team, "user"))
        jingrow.enqueue_pg(
            self.pagetype,
            self.name,
            "_obtain_certificate",
            enqueue_after_commit=True,
            job_id=f"obtain_certificate:{self.name}",
            deduplicate=True,
        )
        jingrow.set_user(user)
        jingrow.session.data = session_data

    @jingrow.whitelist()
    def _obtain_certificate(self):
        """
        实际执行证书获取的核心逻辑。区分 DNS Provider，可能是 Route53 或 DNSPod 等。
        """
        try:
            settings = jingrow.get_pg("Jcloud Settings", "Jcloud Settings")
            ca = LetsEncrypt(settings)
            (
                self.certificate,
                self.full_chain,
                self.intermediate_chain,
                self.private_key,
            ) = ca.obtain(
                domain=self.domain,
                rsa_key_size=self.rsa_key_size,
                wildcard=self.wildcard
            )
            self._extract_certificate_details()
            self.status = "Active"
            self.retry_count = 0
            self.error = None
        except Exception as e:
            if hasattr(e, "output") and e.output:
                output_msg = e.output.decode()
                # 如果 certbot 已在运行，会提示 "Another instance of Certbot is already running"
                if "Another instance of Certbot is already running" in output_msg:
                    time.sleep(5)
                    jingrow.enqueue_pg(
                        self.pagetype,
                        self.name,
                        "_obtain_certificate",
                        job_id=f"obtain_certificate:{self.name}",
                        deduplicate=True,
                    )
                    return
                self.error = output_msg
            else:
                self.error = repr(e)
            self.retry_count += 1
            self.status = "Failure"
            log_error("TLS Certificate Exception", certificate=self.name)

        self.save()
        self.trigger_site_domain_callback()
        self.trigger_self_hosted_server_callback()
        if self.wildcard:
            self.trigger_server_tls_setup_callback()
            self._update_secondary_wildcard_domains()

    def _update_secondary_wildcard_domains(self):
        proxies_containing_domain = jingrow.get_all(
            "Proxy Server Domain", {"domain": self.domain}, pluck="parent"
        )
        proxies_using_domain = jingrow.get_all(
            "Proxy Server", {"domain": self.domain}, pluck="name"
        )
        proxies_containing_domain = set(proxies_containing_domain) - set(proxies_using_domain)
        for proxy_name in proxies_containing_domain:
            proxy = jingrow.get_pg("Proxy Server", proxy_name)
            proxy.setup_wildcard_hosts()

    @jingrow.whitelist()
    def trigger_server_tls_setup_callback(self):
        server_pagetypes = [
            "Proxy Server",
            "Server",
            "Database Server",
            "Log Server",
            "Monitor Server",
            "Registry Server",
            "Analytics Server",
            "Trace Server",
        ]
        for server_pagetype in server_pagetypes:
            servers = jingrow.get_all(
                server_pagetype,
                {"status": "Active", "name": ("like", f"%.{self.domain}")}
            )
            for srv in servers:
                jingrow.enqueue(
                    "jcloud.jcloud.pagetype.tls_certificate.tls_certificate.update_server_tls_certifcate",
                    server=jingrow.get_pg(server_pagetype, srv),
                    certificate=self,
                )

    def trigger_site_domain_callback(self):
        domain = jingrow.db.get_value("Site Domain", {"tls_certificate": self.name}, "name")
        if domain:
            jingrow.get_pg("Site Domain", domain).process_tls_certificate_update()

    def trigger_self_hosted_server_callback(self):
        try:
            jingrow.get_pg("Self Hosted Server", self.name).process_tls_cert_update()
        except Exception:
            pass

    def _extract_certificate_details(self):
        x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, self.certificate)
        self.decoded_certificate = OpenSSL.crypto.dump_certificate(
            OpenSSL.crypto.FILETYPE_TEXT, x509
        ).decode()
        self.issued_on = datetime.strptime(x509.get_notBefore().decode(), "%Y%m%d%H%M%SZ")
        self.expires_on = datetime.strptime(x509.get_notAfter().decode(), "%Y%m%d%H%M%SZ")


get_permission_query_conditions = get_permission_query_conditions_for_pagetype("TLS Certificate")


def renew_tls_certificates():
    """
    定时任务：检查过期时间<25天的证书并自动续期。
    """
    tls_renewal_queue_size = jingrow.db.get_single_value("Jcloud Settings", "tls_renewal_queue_size")
    pending = jingrow.get_all(
        "TLS Certificate",
        fields=["name", "domain", "wildcard", "retry_count"],
        filters={
            "status": ("in", ("Active", "Failure")),
            "expires_on": ("<", jingrow.utils.add_days(None, 25)),
            "retry_count": ("<", 5),
        },
        ignore_ifnull=True,
        order_by="expires_on ASC, status DESC",
    )
    renewals_attempted = 0
    for certificate in pending:
        if tls_renewal_queue_size and (renewals_attempted >= tls_renewal_queue_size):
            break
        site = jingrow.db.get_value("Site Domain", {"tls_certificate": certificate.name}, "site")
        try:
            should_renew = False
            if certificate.wildcard:
                should_renew = True
            else:
                if not site:
                    continue
                if jingrow.db.get_value("Site", site, "status") != "Active":
                    continue
                dns_response = check_dns_cname_a(site, certificate["domain"])
                if dns_response["matched"]:
                    should_renew = True
                else:
                    jingrow.db.set_value(
                        "TLS Certificate",
                        certificate["name"],
                        {
                            "status": "Failure",
                            "error": f"DNS check failed. {dns_response.get('answer')}",
                            "retry_count": certificate["retry_count"] + 1,
                        },
                    )
            if should_renew:
                renewals_attempted += 1
                certificate_pg = jingrow.get_pg("TLS Certificate", certificate["name"])
                certificate_pg._obtain_certificate()
                jingrow.db.commit()
        except Exception as e:
            jingrow.db.rollback()
            jingrow.db.set_value(
                "TLS Certificate",
                certificate["name"],
                {
                    "status": "Failure",
                    "error": repr(e),
                    "retry_count": certificate["retry_count"] + 1,
                },
            )
            log_error("TLS Renewal Exception", certificate=certificate, site=site)
            jingrow.db.commit()


def update_server_tls_certifcate(server, certificate):
    """
    Ansible 部署：将新证书更新到对应服务器。
    """
    try:
        proxysql_admin_password = None
        if server.pagetype == "Proxy Server":
            proxysql_admin_password = server.get_password("proxysql_admin_password")
        ansible = Ansible(
            playbook="tls.yml",
            user=server.get("ssh_user") or "root",
            port=server.get("ssh_port") or 22,
            server=server,
            variables={
                "certificate_private_key": certificate.private_key,
                "certificate_full_chain": certificate.full_chain,
                "certificate_intermediate_chain": certificate.intermediate_chain,
                "is_proxy_server": bool(proxysql_admin_password),
                "proxysql_admin_password": proxysql_admin_password,
            },
        )
        ansible.run()
    except Exception:
        log_error("TLS Setup Exception", server=server.as_dict())


def retrigger_failed_wildcard_tls_callbacks():
    """
    可用于手动重新为失败的通配符证书部署。
    """
    server_pagetypes = [
        "Proxy Server",
        "Server",
        "Database Server",
        "Log Server",
        "Monitor Server",
        "Registry Server",
        "Analytics Server",
        "Trace Server",
    ]
    for server_pagetype in server_pagetypes:
        servers = jingrow.get_all(server_pagetype, {"status": "Active"}, pluck="name")
        for srv in servers:
            plays = jingrow.get_all(
                "Ansible Play",
                {"play": "Setup TLS Certificates", "server": srv},
                pluck="status",
                limit=1,
                order_by="creation DESC",
            )
            if plays and plays[0] != "Success":
                server_pg = jingrow.get_pg(server_pagetype, srv)
                jingrow.enqueue(
                    "jcloud.jcloud.pagetype.tls_certificate.tls_certificate.update_server_tls_certifcate",
                    server=server_pg,
                    certificate=server_pg.get_certificate(),
                )


class LetsEncrypt:
    """
    兼容 Route53 和 DNSPod 的 Certbot 获取逻辑。
    对 DNSPod 采用"生成临时 ini 文件"方式来传递 API 密钥，而不长期保留 .ini。
    """

    def __init__(self, settings):
        self.settings = settings
        self.directory = settings.certbot_directory
        self.webroot_directory = settings.webroot_directory
        self.eff_registration_email = settings.eff_registration_email
        # 开发模式可启用 staging 测试 CA
        self.staging = bool(jingrow.conf.developer_mode and settings.use_staging_ca)

    def obtain(self, domain, rsa_key_size=2048, wildcard=False):
        self.domain = f"*.{domain}" if wildcard else domain
        self.rsa_key_size = rsa_key_size
        self.wildcard = wildcard
        self._obtain()
        return self._extract()

    def _obtain(self):
        """根据域名类型选择验证方式"""
        if not os.path.exists(self.directory):
            os.mkdir(self.directory)
            
        if self.wildcard:
            # 通配符域名需要DNS验证和Root Domain记录
            self._obtain_wildcard()
        else:
            # 普通域名使用HTTP验证(不需要DNS API访问)
            self._obtain_naked()

    def _obtain_wildcard(self):
        """
        处理通配符: 根据 Root Domain 上的 dns_provider 判断：
         - 'AWS Route 53' => 用 AWS_ACCESS_KEY_ID / SECRET
         - 'DNSPod' => 生成临时 ini 文件, 走 -a dns-dnspod
        """
        # 对于通配符证书，直接使用域名而不提取根域名
        domain_to_check = self.domain[2:] if self.domain.startswith("*.") else self.domain
        domain_pg = jingrow.get_pg("Root Domain", domain_to_check)
        provider = domain_pg.dns_provider

        if provider == "AWS Route 53":
            # 原先的 environment 方式:
            env = os.environ.copy()
            env["AWS_ACCESS_KEY_ID"] = domain_pg.aws_access_key_id or ""
            env["AWS_SECRET_ACCESS_KEY"] = domain_pg.get_password("aws_secret_access_key") or ""
            plugin_args = "-a dns-route53"
            command = self._certbot_command(plugin_args)
            self.run(command, environment=env)

        elif provider == "DNSPod":
            # 生成临时 ini 文件
            from tempfile import NamedTemporaryFile
            import stat

            app_id = domain_pg.dnspod_app_id or ""
            app_token = domain_pg.get_password("dnspod_app_token") or ""

            # 1. 创建临时文件
            ini_file = NamedTemporaryFile("w", prefix="dnspod_", suffix=".ini", delete=False)
            ini_path = ini_file.name

            # 2. 写入内容 - 使用正确的参数名称
            ini_file.write(f"dns_dnspod_109_secret_id = {app_id}\n")
            ini_file.write(f"dns_dnspod_109_secret_key = {app_token}\n")
            ini_file.close()

            # 3. chmod 600
            os.chmod(ini_path, stat.S_IRUSR | stat.S_IWUSR)  # 0o600

            try:
                # 使用正确的插件名 dns-dnspod-109
                plugin_args = (
                    f"-a dns-dnspod-109 "
                    f"--dns-dnspod-109-credentials {ini_path} "
                    # 如果需要延迟，可加: --dns-dnspod-109-propagation-seconds 60
                )
                command = self._certbot_command(plugin_args)
                self.run(command)
            finally:
                # 4. 用完后删除临时 ini
                if os.path.exists(ini_path):
                    os.remove(ini_path)

        else:
            raise Exception(f"Unsupported DNS Provider: {provider}")

    def _obtain_naked(self):
        """
        对非通配符场景，默认使用 webroot HTTP-01 验证。您也可加分支区分 Route53 / DNSPod DNS 验证。
        """
        if not os.path.exists(self.webroot_directory):
            os.mkdir(self.webroot_directory)
        plugin_args = f"--webroot --webroot-path {self.webroot_directory}"
        command = self._certbot_command(plugin_args)
        self.run(command)

    def _certbot_command(self, plugin_args):
        """
        拼接 certbot certonly 命令。plugin_args 里可包含 -a dns-dnspod 等。
        """
        staging = "--staging" if self.staging else ""
        force_renewal = "--force-renewal"
        cmd = (
            f"certbot certonly {plugin_args} {staging}"
            f" --logs-dir {self.directory}/logs --work-dir {self.directory}"
            f" --config-dir {self.directory} {force_renewal}"
            f" --agree-tos --eff-email --email {self.eff_registration_email} --staple-ocsp"
            f" --key-type rsa --rsa-key-size {self.rsa_key_size} --cert-name {self.domain}"
            f" --domains {self.domain}"
        )
        return cmd

    def run(self, command, environment=None):
        """
        实际执行 certbot 命令。若失败就抛出异常并记录日志。
        """
        try:
            subprocess.check_output(
                shlex.split(command),
                stderr=subprocess.STDOUT,
                env=environment
            )
        except subprocess.CalledProcessError as e:
            output = (e.output or b"").decode()
            if "Another instance of Certbot is already running" not in output:
                log_error("Certbot Exception", command=command, output=output)
            raise e
        except Exception as e:
            log_error("Certbot Exception", command=command, exception=e)
            raise e

    def _extract(self):
        """
        从certbot生成文件中读取证书、链、私钥等内容并返回。
        """
        base_path = os.path.join(self.directory, "live", self.domain)
        with open(os.path.join(base_path, "cert.pem")) as f:
            certificate = f.read()
        with open(os.path.join(base_path, "fullchain.pem")) as f:
            full_chain = f.read()
        with open(os.path.join(base_path, "chain.pem")) as f:
            intermediate_chain = f.read()
        with open(os.path.join(base_path, "privkey.pem")) as f:
            private_key = f.read()

        return certificate, full_chain, intermediate_chain, private_key
