import argparse
import ipaddress
import sys
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone

from nebula_cert_manager.exceptions import (
    CertManagerError,
    ConfigError,
    RegistryNotFoundError,
)
from nebula_cert_manager.models import NebulaConfig
from nebula_cert_manager.nebula_config import load_nebula_config
from nebula_cert_manager.pki import PKI
from nebula_cert_manager.registry import Registry, RegistryManager


@dataclass
class SyncAction:
    action: str  # "issue", "revoke", "skip"
    name: str
    detail: str


@dataclass
class SyncResult:
    actions: list[SyncAction] = field(default_factory=list)
    issued: int = 0
    revoked: int = 0
    skipped: int = 0


def add_parser(subparsers: argparse._SubParsersAction) -> None:
    parser = subparsers.add_parser(
        "sync",
        help="Reconcile registry with nebula.config.yml",
    )
    parser.add_argument(
        "--expiry-threshold",
        type=int,
        default=100,
        help="Re-issue certs expiring within this many days (default: 100)",
    )
    parser.add_argument(
        "--no-revoke-unknown",
        action="store_true",
        default=False,
        help="Skip revoking certs for hosts not in config",
    )
    parser.add_argument(
        "--no-revoke-old",
        action="store_true",
        default=False,
        help="Skip revoking older active certs (keep only newest)",
    )
    parser.add_argument("--duration", default=None, help="Certificate duration")
    parser.add_argument(
        "--dry-run",
        action="store_true",
        default=False,
        help="Show what would happen without making changes",
    )
    parser.set_defaults(func=run)


def sync_registry(
    registry_mgr: RegistryManager,
    pki: PKI,
    expiry_threshold: int = 100,
    no_revoke_unknown: bool = False,
    no_revoke_old: bool = False,
    duration: str | None = None,
    dry_run: bool = False,
) -> SyncResult:
    """Reconcile registry with nebula.config.yml.

    Collects all actions into SyncResult.
    Raises RegistryNotFoundError, ConfigError.
    """
    if not registry_mgr.exists():
        raise RegistryNotFoundError()

    registry = registry_mgr.load()

    nebula_cfg = load_nebula_config(registry_mgr.registry_dir)
    if nebula_cfg is None or nebula_cfg.hosts is None or nebula_cfg.network is None:
        raise ConfigError(
            "nebula.config.yml is required with 'network' and 'hosts' sections."
        )

    now = datetime.now(timezone.utc)
    threshold = timedelta(days=expiry_threshold)
    cidr = nebula_cfg.network.cidr
    prefix_len = ipaddress.ip_network(cidr, strict=False).prefixlen

    result = SyncResult()
    changed = False
    tag = " [dry run]" if dry_run else ""

    # Phase 1: Issue missing/expiring certs
    for name, host_cfg in nebula_cfg.hosts.hosts.items():
        newest = registry.newest_active_cert(name)
        sign_ip = f"{host_cfg.nebula_ip}/{prefix_len}"
        if newest is None:
            groups = _get_groups(nebula_cfg, name)
            result.actions.append(
                SyncAction("issue", name, f"({sign_ip}) - no active cert{tag}")
            )
            if not dry_run:
                _issue_cert(registry, pki, name, sign_ip, groups, duration)
                changed = True
            result.issued += 1
        elif newest.expires_at <= now + threshold:
            expires_str = newest.expires_at.strftime("%Y-%m-%d")
            groups = _get_groups(nebula_cfg, name)
            result.actions.append(
                SyncAction(
                    "issue",
                    name,
                    f"({sign_ip}) - expires {expires_str} "
                    f"(within {expiry_threshold} days){tag}",
                )
            )
            if not dry_run:
                _issue_cert(registry, pki, name, sign_ip, groups, duration)
                changed = True
            result.issued += 1
        else:
            remaining = (newest.expires_at - now).days
            result.actions.append(
                SyncAction("skip", name, f"(valid for {remaining} more days){tag}")
            )
            result.skipped += 1

    # Phase 2: Revoke old certs per host
    if not no_revoke_old:
        for name in nebula_cfg.hosts.hosts:
            active = registry.active_certs(name)
            if len(active) > 1:
                for old_cert in active[1:]:
                    result.actions.append(
                        SyncAction(
                            "revoke",
                            name,
                            f"(fp: {old_cert.fingerprint})"
                            f" - superseded by newer cert{tag}",
                        )
                    )
                    if not dry_run:
                        old_cert.revoked = True
                        old_cert.revoked_at = now
                        changed = True
                    result.revoked += 1

    # Phase 3: Revoke unknown hosts
    if not no_revoke_unknown:
        config_hosts = set(nebula_cfg.hosts.hosts.keys())
        registry_hosts = set(registry.clients.keys())
        unknown = registry_hosts - config_hosts
        for name in sorted(unknown):
            for cert in registry.active_certs(name):
                result.actions.append(
                    SyncAction(
                        "revoke",
                        name,
                        f"(fp: {cert.fingerprint}) - host not in config{tag}",
                    )
                )
                if not dry_run:
                    cert.revoked = True
                    cert.revoked_at = now
                    changed = True
                result.revoked += 1

    # Phase 4: Save once
    if changed and not dry_run:
        registry_mgr.save(registry)

    return result


def run(args: argparse.Namespace) -> None:
    try:
        result = sync_registry(
            args.registry_mgr,
            args.pki,
            args.expiry_threshold,
            args.no_revoke_unknown,
            args.no_revoke_old,
            args.duration,
            args.dry_run,
        )
    except CertManagerError as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

    for action in result.actions:
        print(f"  {action.action:<8}{action.name} {action.detail}")

    dry_suffix = " (dry run)" if args.dry_run else ""
    print(
        f"\nSync complete: {result.issued} issued, {result.revoked} revoked, "
        f"{result.skipped} skipped{dry_suffix}"
    )


def _get_groups(nebula_cfg: NebulaConfig, name: str) -> list[str]:
    groups: list[str] = []
    if nebula_cfg.firewall is not None:
        groups.extend(nebula_cfg.firewall.host_groups.get(name, []))
    return groups


def _issue_cert(
    registry: Registry,
    pki: PKI,
    name: str,
    sign_ip: str,
    groups: list[str],
    duration: str | None,
) -> None:
    client = pki.sign_and_build_client_info(
        ca_cert=registry.ca.cert,
        ca_key=registry.ca.key,
        name=name,
        ip=sign_ip,
        groups=groups,
        duration=duration,
    )
    registry.clients.setdefault(name, []).append(client)
