# root_domain.py

from __future__ import annotations

import json
from datetime import datetime, timedelta
from typing import Iterable

import boto3
import jingrow
import requests
from jingrow.core.utils import find
from jingrow.model.page import Page

from jcloud.utils import log_error


class RootDomain(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

        aws_access_key_id: DF.Data | None
        aws_secret_access_key: DF.Password | None
        default_cluster: DF.Link
        dns_provider: DF.Literal["DNSPod", "AWS Route 53", "Other"]
        dnspod_app_id: DF.Data | None
        dnspod_app_token: DF.Password | None
    # end: auto-generated types

    def after_insert(self):
        """
        创建 RootDomain 后自动创建一条通配符证书记录，以获取 *.domain.com 证书。
        """
        if not jingrow.db.exists("TLS Certificate", {"wildcard": True, "domain": self.name}):
            jingrow.enqueue_pg(
                self.pagetype,
                self.name,
                "obtain_root_domain_tls_certificate",
                enqueue_after_commit=True,
            )

    def obtain_root_domain_tls_certificate(self):
        """
        实际队列执行，创建 TLS Certificate 文档，让系统自动签发通配符证书。
        """
        try:
            rsa_key_size = jingrow.db.get_value("Jcloud Settings", "Jcloud Settings", "rsa_key_size")
            jingrow.get_pg(
                {
                    "pagetype": "TLS Certificate",
                    "wildcard": True,
                    "domain": self.name,
                    "rsa_key_size": rsa_key_size,
                }
            ).insert()
        except Exception:
            log_error("Root Domain TLS Certificate Exception")

    # ----------------------------------------------------------------
    # Route53 原有逻辑
    # ----------------------------------------------------------------
    @property
    def boto3_client(self):
        """
        只有当 dns_provider == 'AWS Route 53' 时，才初始化 boto3 route53 客户端。
        """
        if self.dns_provider != "AWS Route 53":
            return None
        if not hasattr(self, "_boto3_client"):
            self._boto3_client = boto3.client(
                "route53",
                aws_access_key_id=self.aws_access_key_id,
                aws_secret_access_key=self.get_password("aws_secret_access_key"),
            )
        return self._boto3_client

    @property
    def hosted_zone(self):
        """
        仅当 dns_provider == 'AWS Route 53' 时使用，获取 Route53 HostedZone Id。
        """
        if self.dns_provider != "AWS Route 53":
            return None
        zones = self.boto3_client.list_hosted_zones_by_name()["HostedZones"]
        return find(reversed(zones), lambda x: self.name.endswith(x["Name"][:-1]))["Id"]

    # ----------------------------------------------------------------
    # DNSPod 所需：合并 dnspod_app_id + dnspod_app_token => login_token
    # ----------------------------------------------------------------
    @property
    def dnspod_login_token(self) -> str:
        """
        DNSPod API 需要将 AppID 和 Token 拼成 "AppID,Token" 格式。
        """
        app_id = self.dnspod_app_id or ""
        app_token = self.get_password("dnspod_app_token") or ""
        return f"{app_id},{app_token}"

    # ----------------------------------------------------------------
    # 通用 DNS 查询 / 删除 / 更新，区分 Route53 和 DNSPod
    # ----------------------------------------------------------------
    def get_dns_record_pages(self) -> Iterable:
        """
        根据 dns_provider 不同，分别获取 DNS 记录。
        - AWS Route53：使用 boto3 分页
        - DNSPod：一次性获取所有记录，然后 yield 仿照分页结构
        """
        try:
            if self.dns_provider == "AWS Route 53":
                paginator = self.boto3_client.get_paginator("list_resource_record_sets")
                return paginator.paginate(
                    PaginationConfig={"MaxItems": 10000, "PageSize": 300, "StartingToken": "0"},
                    HostedZoneId=self.hosted_zone.split("/")[-1],
                )
            elif self.dns_provider == "DNSPod":
                record_list = self._dnspod_list_records()
                yield {"ResourceRecordSets": record_list}
            else:
                # 其他 provider 可自行扩展
                return []
        except Exception:
            log_error(f"{self.dns_provider} Pagination Error", domain=self.name)
            return []

    def delete_dns_records(self, records: list[str]):
        """
        统一的删除入口：根据 dns_provider == 'AWS Route 53' or 'DNSPod' 执行不同操作。
        """
        try:
            if self.dns_provider == "AWS Route 53":
                changes = []
                for record in records:
                    changes.append({"Action": "DELETE", "ResourceRecordSet": record})
                self.boto3_client.change_resource_record_sets(
                    ChangeBatch={"Changes": changes}, HostedZoneId=self.hosted_zone
                )
            elif self.dns_provider == "DNSPod":
                for record in records:
                    self._dnspod_delete_record(record)
        except Exception:
            log_error(f"{self.dns_provider} Record Deletion Error", domain=self.name)

    def remove_unused_cname_records(self):
        """
        原先 Route53 清理逻辑保留，DNSPod 同理处理。
        """
        proxies = jingrow.get_all("Proxy Server", {"status": "Active"}, pluck="name")
        for page in self.get_dns_record_pages() or []:
            to_delete = []
            active = self.get_active_domains()
            jingrow.db.commit()

            for record in page["ResourceRecordSets"]:
                # 结构:
                # {
                #   "Name": "xxx",
                #   "Type": "CNAME",
                #   "ResourceRecords": [{"Value": "proxy"}],
                #   ...
                # }
                if record["Type"] == "CNAME" and record["ResourceRecords"][0]["Value"] in proxies:
                    domain = record["Name"].strip(".")
                    if domain not in active:
                        record["Name"] = domain
                        to_delete.append(record)
            if to_delete:
                self.delete_dns_records(to_delete)

    def update_dns_records_for_sites(self, sites: list[str], proxy_server: str):
        """
        批量为多个域名 CNAME 指向 proxy_server。
        对 AWS Route53 => UPSERT
        对 DNSPod   => Record.Modify / Create
        """
        batch_size = 500
        for i in range(0, len(sites), batch_size):
            chunk = sites[i : i + batch_size]
            if self.dns_provider == "AWS Route 53":
                changes = []
                for site in chunk:
                    changes.append({
                        "Action": "UPSERT",
                        "ResourceRecordSet": {
                            "Name": site,
                            "Type": "CNAME",
                            "TTL": 600,
                            "ResourceRecords": [{"Value": proxy_server}],
                        },
                    })
                self.boto3_client.change_resource_record_sets(
                    ChangeBatch={"Changes": changes}, HostedZoneId=self.hosted_zone
                )
            elif self.dns_provider == "DNSPod":
                for site in chunk:
                    self._dnspod_upsert_record(
                        record_name=site,
                        record_type="CNAME",
                        record_value=proxy_server,
                        ttl=600,
                    )

    # ----------------------------------------------------------------
    # 查找活跃/被改名站点，以判断是否删除解析
    # ----------------------------------------------------------------
    def get_sites_being_renamed(self):
        last_hour = datetime.now() - timedelta(hours=1)
        renaming_sites = jingrow.get_all(
            "Agent Job",
            {"job_type": "Rename Site", "creation": (">=", last_hour)},
            pluck="request_data",
        )
        return [json.loads(d_str)["new_name"] for d_str in renaming_sites]

    def get_active_site_domains(self):
        return jingrow.get_all(
            "Site Domain", {"domain": ("like", f"%{self.name}"), "status": "Active"}, pluck="name"
        )

    def get_active_domains(self):
        active_sites = jingrow.get_all(
            "Site", {"status": ("!=", "Archived"), "domain": self.name}, pluck="name"
        )
        active_sites.extend(self.get_sites_being_renamed())
        active_sites.extend(self.get_active_site_domains())
        return active_sites

    # ----------------------------------------------------------------
    # DNSPod 相关调用
    # ----------------------------------------------------------------
    def _dnspod_list_records(self):
        """
        DNSPod: Record.List
        https://www.dnspod.cn/docs/records/record-list
        """
        token = self.dnspod_login_token  # "APPID,APPTOKEN"
        domain_name = self.name
        url = "https://dnsapi.cn/Record.List"
        data = {
            "login_token": token,
            "format": "json",
            "domain": domain_name,
        }
        resp = requests.post(url, data=data).json()
        if resp["status"]["code"] != "1":
            raise Exception(f"DNSPod List Records Failed: {resp['status']['message']}")

        record_list = []
        for r in resp["records"]:
            record_list.append({
                "Name": r["name"] + "." + domain_name,
                "Type": r["type"],
                "ResourceRecords": [{"Value": r["value"]}],
                "TTL": r["ttl"],
                "RecordId": r["id"],
            })
        return record_list

    def _dnspod_delete_record(self, record):
        """
        DNSPod: Record.Remove
        https://www.dnspod.cn/docs/records/record-delete
        """
        token = self.dnspod_login_token
        domain_name = self.name
        record_id = record.get("RecordId")
        if not record_id:
            return

        url = "https://dnsapi.cn/Record.Remove"
        data = {
            "login_token": token,
            "format": "json",
            "domain": domain_name,
            "record_id": record_id,
        }
        resp = requests.post(url, data=data).json()
        if resp["status"]["code"] != "1":
            raise Exception(f"DNSPod Delete Record Failed: {resp['status']['message']}")

    def _dnspod_upsert_record(self, record_name: str, record_type: str, record_value: str, ttl=600):
        """
        DNSPod: 先 list 再判断是 Modify 还是 Create。
        https://www.dnspod.cn/docs/records/record-modify
        https://www.dnspod.cn/docs/records/record-create
        """
        token = self.dnspod_login_token
        domain_name = self.name
        sub_name = record_name.replace("." + domain_name, "")

        all_records = self._dnspod_list_records()
        existing = None
        for rec in all_records:
            if rec["Name"].strip(".") == record_name.strip(".") and rec["Type"] == record_type:
                existing = rec
                break

        if existing:
            # Record.Modify
            url = "https://dnsapi.cn/Record.Modify"
            data = {
                "login_token": token,
                "format": "json",
                "domain": domain_name,
                "record_id": existing["RecordId"],
                "sub_domain": sub_name,
                "record_type": record_type,
                "record_line": "默认",
                "value": record_value,
                "ttl": ttl,
            }
        else:
            # Record.Create
            url = "https://dnsapi.cn/Record.Create"
            data = {
                "login_token": token,
                "format": "json",
                "domain": domain_name,
                "sub_domain": sub_name,
                "record_type": record_type,
                "record_line": "默认",
                "value": record_value,
                "ttl": ttl,
            }

        resp = requests.post(url, data=data).json()
        if resp["status"]["code"] != "1":
            raise Exception(f"DNSPod Upsert Record Failed: {resp['status']['message']}")


def cleanup_cname_records():
    """
    与 TLS Certificate 等配合，用于定期清理不再使用的 CNAME 记录。
    """
    domains = jingrow.get_all("Root Domain", pluck="name")
    for domain_name in domains:
        domain = RootDomain("Root Domain", domain_name)
        domain.remove_unused_cname_records()
