import json
from typing import List

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from sqlalchemy.orm import Session

from .. import models, schemas
from ..crypto import encrypt_token
from ..db import get_db
from ..dns_providers import DNSProviderError, publish_dkim_record_for_domain
from ..cache import CACHE_TTL_SECONDS, get_cache, safe_cache_delete, safe_cache_set
from ..security import require_admin
from ..settings import get_settings
from ..utils import generate_dkim_key, remove_managed_dkim_key, sync_dkim_signing_maps

router = APIRouter(prefix="/domains", tags=["domains"])
settings = get_settings()


def cache_domain(cache, domain: models.Domain) -> None:
    safe_cache_set(
        cache,
        f"domain:{domain.name}",
        json.dumps(
            {
                "id": domain.id,
                "name": domain.name,
                "is_active": domain.is_active,
                "max_users": domain.max_users,
                "dkim_selector": domain.dkim_selector,
                "dkim_public_key": domain.dkim_public_key,
                "dmarc_policy": domain.dmarc_policy,
                "dns_provider": domain.dns_provider,
                "dns_account_id": domain.dns_account_id,
                "dns_zone_id": domain.dns_zone_id,
                "dns_sync_enabled": domain.dns_sync_enabled,
                "dns_last_sync_at": domain.dns_last_sync_at.isoformat() if domain.dns_last_sync_at else None,
                "dns_last_sync_status": domain.dns_last_sync_status,
            }
        ),
        ex=CACHE_TTL_SECONDS,
    )


def get_domain_or_404(db: Session, domain_id: int) -> models.Domain:
    domain = db.query(models.Domain).filter(models.Domain.id == domain_id).first()
    if not domain:
        raise HTTPException(status_code=404, detail="Domain not found")
    return domain


def _set_dns_sync_status(domain: models.Domain, status_value: str) -> None:
    domain.dns_last_sync_at = models.utc_now()
    domain.dns_last_sync_status = status_value[:512]


def _publish_domain_dkim(domain: models.Domain, db: Session, cache) -> dict[str, object]:
    try:
        change = publish_dkim_record_for_domain(domain)
    except DNSProviderError as exc:
        _set_dns_sync_status(domain, f"error: {exc}")
        db.commit()
        cache_domain(cache, domain)
        raise HTTPException(status_code=502, detail=str(exc)) from exc
    _set_dns_sync_status(domain, f"ok: {change.action} {change.name}")
    db.commit()
    db.refresh(domain)
    cache_domain(cache, domain)
    return change.as_dict()


def _try_auto_publish_domain_dkim(domain: models.Domain, db: Session, cache) -> dict[str, object] | None:
    if not domain.dns_sync_enabled:
        return None
    try:
        return _publish_domain_dkim(domain, db, cache)
    except HTTPException as exc:
        return {"status": "error", "detail": exc.detail}


@router.get("/", response_model=List[schemas.DomainOut])
def list_domains(db: Session = Depends(get_db), _: str = Depends(require_admin)):
    return db.query(models.Domain).all()


@router.post("/", response_model=schemas.DomainOut)
def create_domain(payload: schemas.DomainCreate, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    existing = db.query(models.Domain).filter(models.Domain.name == payload.name).first()
    if existing:
        raise HTTPException(status_code=400, detail="Domain already exists")
    domain = models.Domain(
        name=payload.name,
        is_active=payload.is_active,
        max_users=payload.max_users,
        dmarc_policy=payload.dmarc_policy,
        dkim_selector=payload.dkim_selector,
    )
    if payload.generate_dkim:
        dkim = generate_dkim_key(payload.name, payload.dkim_selector)
        domain.dkim_private_path = dkim["path"]
        domain.dkim_public_key = dkim.get("dns")
    db.add(domain)
    try:
        db.commit()
    except IntegrityError as exc:
        db.rollback()
        if domain.dkim_private_path:
            remove_managed_dkim_key(domain.dkim_private_path)
        raise HTTPException(status_code=409, detail="Domain already exists") from exc
    db.refresh(domain)
    sync_dkim_signing_maps(db.query(models.Domain).all())
    cache_domain(cache, domain)
    return domain


@router.get("/{domain_id}", response_model=schemas.DomainOut)
def get_domain(domain_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    return get_domain_or_404(db, domain_id)


@router.patch("/{domain_id}", response_model=schemas.DomainOut)
def update_domain(domain_id: int, payload: schemas.DomainUpdate, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    domain = get_domain_or_404(db, domain_id)
    if payload.is_active is not None:
        domain.is_active = payload.is_active
    if payload.max_users is not None:
        domain.max_users = payload.max_users
    if payload.dmarc_policy is not None:
        domain.dmarc_policy = payload.dmarc_policy
    db.commit()
    db.refresh(domain)
    cache_domain(cache, domain)
    return domain


@router.post("/{domain_id}/dns/provider", response_model=schemas.DomainOut)
def configure_dns_provider(
    domain_id: int,
    payload: schemas.DomainDnsProviderUpdate,
    db: Session = Depends(get_db),
    cache=Depends(get_cache),
    _: str = Depends(require_admin),
):
    domain = get_domain_or_404(db, domain_id)
    provider = payload.dns_provider
    if provider == "none":
        domain.dns_provider = None
        domain.dns_account_id = None
        domain.dns_zone_id = None
        domain.dns_api_token = None
        domain.dns_sync_enabled = False
        domain.dns_last_sync_status = "disabled"
    else:
        domain.dns_provider = provider or "cloudflare"
        if payload.dns_account_id is not None:
            domain.dns_account_id = payload.dns_account_id
        if payload.dns_zone_id is not None:
            domain.dns_zone_id = payload.dns_zone_id
        if payload.clear_api_token:
            domain.dns_api_token = None
        elif payload.dns_api_token:
            domain.dns_api_token = encrypt_token(payload.dns_api_token)
        if payload.dns_sync_enabled is not None:
            domain.dns_sync_enabled = payload.dns_sync_enabled
        if domain.dns_sync_enabled and (not domain.dns_account_id or not domain.dns_zone_id or not domain.dns_api_token):
            raise HTTPException(status_code=400, detail="Cloudflare account ID, zone ID and API token are required before enabling DNS sync")
    db.commit()
    db.refresh(domain)
    cache_domain(cache, domain)
    return domain


@router.post("/{domain_id}/dkim", response_model=dict)
def rotate_dkim(domain_id: int, selector: str = "default", db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    domain = get_domain_or_404(db, domain_id)
    old_private_path = domain.dkim_private_path
    dkim = generate_dkim_key(domain.name, selector)
    domain.dkim_private_path = dkim["path"]
    domain.dkim_public_key = dkim.get("dns")
    domain.dkim_selector = selector
    try:
        db.commit()
    except SQLAlchemyError as exc:
        db.rollback()
        if dkim.get("path") != old_private_path:
            remove_managed_dkim_key(dkim.get("path"))
        raise HTTPException(status_code=500, detail="Unable to save the rotated DKIM key") from exc
    db.refresh(domain)
    if old_private_path and old_private_path != domain.dkim_private_path:
        remove_managed_dkim_key(old_private_path)
    sync_dkim_signing_maps(db.query(models.Domain).all())
    cache_domain(cache, domain)
    dns_sync = _try_auto_publish_domain_dkim(domain, db, cache)
    return {"dns_record": dkim.get("dns"), "selector": selector, "path": dkim.get("path"), "dns_sync": dns_sync}


@router.post("/{domain_id}/dns/sync-dkim", response_model=dict)
def sync_dkim_dns(domain_id: int, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    domain = get_domain_or_404(db, domain_id)
    result = _publish_domain_dkim(domain, db, cache)
    return {"status": "ok", "record": result}


@router.delete("/{domain_id}", response_model=dict)
def delete_domain(domain_id: int, db: Session = Depends(get_db), cache=Depends(get_cache), _: str = Depends(require_admin)):
    domain = get_domain_or_404(db, domain_id)
    domain_name = domain.name
    for account in domain.accounts:
        safe_cache_delete(cache, f"account:{account.local_part}@{domain_name}")
    for alias in domain.aliases:
        safe_cache_delete(cache, f"alias:{alias.source_local}@{domain_name}")
    for redirect in domain.redirects:
        safe_cache_delete(cache, f"redirect:{redirect.source_local}@{domain_name}")
    safe_cache_delete(cache, f"domain:{domain_name}")
    private_key_path = domain.dkim_private_path
    db.delete(domain)
    db.commit()
    remove_managed_dkim_key(private_key_path)
    sync_dkim_signing_maps(db.query(models.Domain).all())
    return {"deleted": True, "domain": domain_name}


@router.get("/{domain_id}/dns", response_model=dict)
def suggested_dns(domain_id: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    domain = get_domain_or_404(db, domain_id)
    mx_host = settings.hostname
    mail_host_record = f"mail.{domain.name}"
    spf = f"v=spf1 mx a:{mx_host} -all"
    dmarc = f"v=DMARC1; p={domain.dmarc_policy}; adkim=s; aspf=s; pct=100; rua=mailto:postmaster@{domain.name}"
    dkim = domain.dkim_public_key or "Generate a DKIM key to populate this record"
    arc = "ARC enabled via Rspamd (no DNS record needed). Keep DKIM signing active for forwarded mail."
    payload = {
        "mx": f"{domain.name} IN MX 10 {mx_host}.",
        **({"mail": f"{mail_host_record} IN CNAME {mx_host}."} if mail_host_record != mx_host else {}),
        "spf": f"{domain.name} IN TXT \"{spf}\"",
        **(
            {"srs_spf": f"{settings.srs_domain} IN TXT \"{spf}\""}
            if settings.enable_srs
            and settings.srs_domain
            and settings.srs_domain != domain.name
            and domain.name == settings.primary_domain
            else {}
        ),
        "dmarc": f"_dmarc.{domain.name} IN TXT \"{dmarc}\"",
        "dkim": f"{domain.dkim_selector}._domainkey.{domain.name} IN TXT \"{dkim}\"",
        "jmap": f"_jmap._tcp.{domain.name} IN SRV 0 1 443 {mx_host}.",
        "imap": f"_imap._tcp.{domain.name} IN SRV 0 1 143 {mx_host}.",
        "imaps": f"_imaps._tcp.{domain.name} IN SRV 0 1 993 {mx_host}.",
        "pop3": f"_pop3._tcp.{domain.name} IN SRV 0 1 110 {mx_host}.",
        "pop3s": f"_pop3s._tcp.{domain.name} IN SRV 0 1 995 {mx_host}.",
        "submission": f"_submission._tcp.{domain.name} IN SRV 0 1 587 {mx_host}.",
        "submissions": f"_submissions._tcp.{domain.name} IN SRV 0 1 465 {mx_host}.",
        "autoconfig": f"autoconfig.{domain.name} IN CNAME {mx_host}.",
        "autodiscover": f"autodiscover.{domain.name} IN CNAME {mx_host}.",
        "arc": arc,
        "ptr": f"Set the reverse DNS of the public mail IP to {mx_host}.",
    }
    if settings.enable_mta_sts and domain.name == settings.primary_domain:
        if settings.mta_sts_host != mx_host:
            payload["mta_sts_host"] = f"{settings.mta_sts_host} IN CNAME {mx_host}."
        payload["mta_sts"] = f"_mta-sts.{domain.name} IN TXT \"v=STSv1; id={settings.mta_sts_id}\""
        payload["mta_sts_policy"] = f"https://{settings.mta_sts_host}/.well-known/mta-sts.txt"
    if settings.enable_tls_rpt and domain.name == settings.primary_domain:
        payload["tls_rpt"] = f"_smtp._tls.{domain.name} IN TXT \"v=TLSRPTv1; rua=mailto:{settings.tls_rpt_mailbox}\""
    return payload
