"""Read-only, privacy-bounded production deployment qualification checks."""

from __future__ import annotations

import base64
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import MappingProxyType
from typing import Mapping, Optional

from alembic.config import Config
from alembic.runtime.migration import MigrationContext
from alembic.script import ScriptDirectory
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from sqlalchemy import Engine, func, inspect, select, text
from sqlalchemy.orm import Session

from licensing_shared.catalog import LicensingCatalog
from licensing_shared.constants import PRODUCT_ID, validate_identifier
from licensing_shared.verifier import ED25519_PUBLIC_KEY_BYTES, LicenseVerifier

from .config import (
    NotificationWorkerSettings,
    ServerSettings,
    SubscriptionWorkerSettings,
)
from .catalog_administration import inspect_catalog_registry
from .database import Base
from .models import NotificationDelivery, OutboxEvent
from .security import SnapshotSigner
from .signing_key_administration import inspect_signing_key_registry


DEPLOYMENT_REPORT_SCHEMA = "apolon.licensing.deployment-readiness"
DEPLOYMENT_REPORT_SCHEMA_VERSION = 1
DEFAULT_MAXIMUM_BACKLOG_AGE_MINUTES = 15
MAXIMUM_BACKLOG_AGE_MINUTES = 7 * 24 * 60
ALEMBIC_CONFIGURATION_PATH = Path(__file__).resolve().parents[1] / "alembic.ini"
CHECK_STATUS_PASSED = "passed"
CHECK_STATUS_FAILED = "failed"
CHECK_STATUSES = frozenset((CHECK_STATUS_PASSED, CHECK_STATUS_FAILED))
READINESS_STATUS_READY = "ready"
READINESS_STATUS_FAILED = "failed"
SIGNER_PROBE_SCHEMA = "apolon.licensing.signer-readiness-probe"
SIGNER_PROBE_SCHEMA_VERSION = 1
DEPLOYMENT_COMPONENT_API = "api"
DEPLOYMENT_COMPONENT_SUBSCRIPTION_WORKER = "subscription_worker"
DEPLOYMENT_COMPONENT_NOTIFICATION_WORKER = "notification_worker"
DEPLOYMENT_COMPONENTS = frozenset(
    (
        DEPLOYMENT_COMPONENT_API,
        DEPLOYMENT_COMPONENT_SUBSCRIPTION_WORKER,
        DEPLOYMENT_COMPONENT_NOTIFICATION_WORKER,
    )
)


@dataclass(frozen=True)
class DeploymentReadinessCheck:
    name: str
    status: str
    code: str
    details: Mapping[str, object]

    def __post_init__(self) -> None:
        object.__setattr__(self, "name", validate_identifier(self.name, "check name"))
        if not isinstance(self.status, str) or self.status not in CHECK_STATUSES:
            raise ValueError("check status is invalid")
        object.__setattr__(self, "code", validate_identifier(self.code, "check code"))
        if not isinstance(self.details, Mapping):
            raise TypeError("check details must be a mapping")
        object.__setattr__(self, "details", MappingProxyType(dict(self.details)))

    def to_mapping(self) -> dict[str, object]:
        return {
            "name": self.name,
            "status": self.status,
            "code": self.code,
            "details": dict(self.details),
        }


@dataclass(frozen=True)
class DeploymentReadinessReport:
    checked_at: datetime
    production_required: bool
    component: str
    checks: tuple[DeploymentReadinessCheck, ...]

    def __post_init__(self) -> None:
        object.__setattr__(self, "checked_at", _utc(self.checked_at))
        if not isinstance(self.production_required, bool):
            raise TypeError("production_required must be a Boolean")
        if not isinstance(self.component, str) or self.component not in DEPLOYMENT_COMPONENTS:
            raise ValueError("component is invalid")
        if not isinstance(self.checks, tuple) or not self.checks:
            raise ValueError("checks must be a non-empty tuple")
        if any(not isinstance(value, DeploymentReadinessCheck) for value in self.checks):
            raise TypeError("checks must contain DeploymentReadinessCheck values")

    @property
    def ready(self) -> bool:
        return all(value.status == CHECK_STATUS_PASSED for value in self.checks)

    def to_mapping(self) -> dict[str, object]:
        return {
            "schema": DEPLOYMENT_REPORT_SCHEMA,
            "schemaVersion": DEPLOYMENT_REPORT_SCHEMA_VERSION,
            "checkedAt": self.checked_at.isoformat().replace("+00:00", "Z"),
            "qualificationMode": (
                "production" if self.production_required else "nonproduction_diagnostic"
            ),
            "component": self.component,
            "status": READINESS_STATUS_READY if self.ready else READINESS_STATUS_FAILED,
            "checks": [value.to_mapping() for value in self.checks],
        }


def inspect_deployment_readiness(
    engine: Engine,
    settings: (
        ServerSettings | SubscriptionWorkerSettings | NotificationWorkerSettings
    ),
    catalog: LicensingCatalog,
    signer: Optional[SnapshotSigner],
    *,
    now: datetime,
    production_required: bool = True,
    require_commerce: bool = False,
    maximum_backlog_age_minutes: int = DEFAULT_MAXIMUM_BACKLOG_AGE_MINUTES,
    alembic_configuration_path: Optional[Path] = None,
    component: str = DEPLOYMENT_COMPONENT_API,
) -> DeploymentReadinessReport:
    if not isinstance(engine, Engine):
        raise TypeError("engine must be an Engine")
    if not isinstance(catalog, LicensingCatalog):
        raise TypeError("catalog must be LicensingCatalog")
    normalized_component = validate_identifier(component, "component")
    if normalized_component not in DEPLOYMENT_COMPONENTS:
        raise ValueError("component is unsupported")
    if normalized_component == DEPLOYMENT_COMPONENT_API:
        if not isinstance(settings, ServerSettings):
            raise TypeError("the API doctor requires ServerSettings")
    elif normalized_component == DEPLOYMENT_COMPONENT_SUBSCRIPTION_WORKER:
        if not isinstance(settings, SubscriptionWorkerSettings):
            raise TypeError(
                "the subscription-worker doctor requires SubscriptionWorkerSettings"
            )
    elif not isinstance(settings, NotificationWorkerSettings):
        raise TypeError(
            "the notification-worker doctor requires NotificationWorkerSettings"
        )
    if require_commerce and normalized_component != DEPLOYMENT_COMPONENT_API:
        raise ValueError("require_commerce applies only to the API component")
    if signer is not None and (
        not all(
            callable(getattr(signer, member, None))
            for member in ("public_key_bytes", "sign_payload")
        )
        or not isinstance(getattr(signer, "key_id", None), str)
    ):
        raise TypeError("signer does not implement SnapshotSigner")
    if normalized_component == DEPLOYMENT_COMPONENT_API and signer is None:
        raise ValueError("the API deployment doctor requires a signer")
    for field_name, value in (
        ("production_required", production_required),
        ("require_commerce", require_commerce),
    ):
        if not isinstance(value, bool):
            raise TypeError(f"{field_name} must be a Boolean")
    normalized_now = _utc(now)
    backlog_age = _bounded_integer(
        maximum_backlog_age_minutes,
        "maximum_backlog_age_minutes",
        MAXIMUM_BACKLOG_AGE_MINUTES,
    )
    migration_path = (
        ALEMBIC_CONFIGURATION_PATH
        if alembic_configuration_path is None
        else alembic_configuration_path
    )
    if not isinstance(migration_path, Path):
        raise TypeError("alembic_configuration_path must be a Path or None")
    if not migration_path.is_file() or migration_path.is_symlink():
        raise ValueError("alembic_configuration_path must be a regular file")

    checks: list[DeploymentReadinessCheck] = []
    environment_is_production = settings.environment_name.strip().lower() == "production"
    checks.append(
        _check(
            "environment",
            not production_required or environment_is_production,
            "environment_valid",
            "environment_not_production",
            {
                "environment": settings.environment_name.strip().lower(),
                "productionRequired": production_required,
            },
        )
    )
    database_is_postgresql = engine.dialect.name == "postgresql"
    checks.append(
        _check(
            "database_dialect",
            not production_required or database_is_postgresql,
            "database_dialect_valid",
            "database_dialect_not_postgresql",
            {
                "dialect": engine.dialect.name,
                "productionRequired": production_required,
            },
        )
    )
    checks.append(_catalog_check(catalog))
    if normalized_component == DEPLOYMENT_COMPONENT_API:
        native_identity_ready = all(
            isinstance(value, str) and bool(value.strip())
            for value in (
                settings.oidc_authorization_endpoint,
                settings.oidc_token_endpoint,
                settings.oidc_native_client_id,
            )
        )
        checks.append(
            _check(
                "native_identity",
                native_identity_ready,
                "native_identity_configured",
                "native_identity_incomplete",
                {"configured": native_identity_ready},
            )
        )
        checks.append(
            _check(
                "commerce",
                not require_commerce or settings.stripe_commerce_enabled,
                (
                    "commerce_configured"
                    if settings.stripe_commerce_enabled
                    else "commerce_not_required"
                ),
                "commerce_required_but_disabled",
                {
                    "enabled": settings.stripe_commerce_enabled,
                    "required": require_commerce,
                },
            )
        )
        assert signer is not None
        checks.append(_signer_check(signer, normalized_now))
    elif normalized_component == DEPLOYMENT_COMPONENT_SUBSCRIPTION_WORKER:
        provider_ready = bool(settings.stripe_secret_key)
        checks.append(
            _check(
                "subscription_provider",
                provider_ready,
                "subscription_provider_configured",
                "subscription_provider_incomplete",
                {"configured": provider_ready},
            )
        )
    else:
        notification_ready = settings.email_notifications_enabled
        checks.append(
            _check(
                "notifications",
                notification_ready,
                "notifications_configured",
                "notifications_incomplete",
                {"configured": notification_ready},
            )
        )
    checks.extend(
        _database_checks(
            engine,
            migration_path,
            normalized_now,
            timedelta(minutes=backlog_age),
        )
    )
    if normalized_component == DEPLOYMENT_COMPONENT_API:
        assert signer is not None
        checks.append(_catalog_registry_check(engine, catalog))
        checks.append(_signing_registry_check(engine, signer, normalized_now))
    elif normalized_component == DEPLOYMENT_COMPONENT_SUBSCRIPTION_WORKER:
        checks.append(_catalog_registry_check(engine, catalog))
    return DeploymentReadinessReport(
        checked_at=normalized_now,
        production_required=production_required,
        component=normalized_component,
        checks=tuple(checks),
    )


def _catalog_check(catalog: LicensingCatalog) -> DeploymentReadinessCheck:
    valid = catalog.product_id == PRODUCT_ID and catalog.revision > 0 and bool(catalog.skus)
    return _check(
        "catalog",
        valid,
        "catalog_valid",
        "catalog_invalid",
        {
            "productId": catalog.product_id,
            "revision": catalog.revision,
            "skuCount": len(catalog.skus),
        },
    )


def _signer_check(
    signer: SnapshotSigner,
    now: datetime,
) -> DeploymentReadinessCheck:
    try:
        key_id = validate_identifier(signer.key_id, "signer key_id")
        public_key_bytes = signer.public_key_bytes()
        if (
            not isinstance(public_key_bytes, bytes)
            or len(public_key_bytes) != ED25519_PUBLIC_KEY_BYTES
        ):
            raise ValueError("signer public key is invalid")
        payload = {
            "schema": SIGNER_PROBE_SCHEMA,
            "schemaVersion": SIGNER_PROBE_SCHEMA_VERSION,
            "checkedAt": now.isoformat().replace("+00:00", "Z"),
        }
        signed = signer.sign_payload(payload)
        if signed.key_id != key_id or signed.payload != payload:
            raise ValueError("signer response binding is invalid")
        signature = base64.b64decode(signed.signature_base64, validate=True)
        Ed25519PublicKey.from_public_bytes(public_key_bytes).verify(
            signature,
            LicenseVerifier.signing_bytes(payload),
        )
    except Exception:
        return _check(
            "signer",
            False,
            "signer_operational",
            "signer_probe_failed",
            {"operational": False},
        )
    return _check(
        "signer",
        True,
        "signer_operational",
        "signer_probe_failed",
        {"keyId": key_id, "operational": True},
    )


def _signing_registry_check(
    engine: Engine,
    signer: SnapshotSigner,
    now: datetime,
) -> DeploymentReadinessCheck:
    try:
        with Session(engine) as session:
            status = inspect_signing_key_registry(session, signer, now)
    except Exception:
        return _check(
            "signing_key_registry",
            False,
            "signing_key_registry_ready",
            "signing_key_registry_unavailable",
            {"issuanceReady": False},
        )
    ready = status.get("issuanceReady") is True
    keys = status.get("keys")
    return _check(
        "signing_key_registry",
        ready,
        "signing_key_registry_ready",
        "signing_key_registry_not_ready",
        {
            "activeKeyId": status.get("activeKeyId"),
            "configuredKeyId": status.get("configuredKeyId"),
            "configuredKeyStatus": status.get("configuredKeyStatus"),
            "issuanceReady": ready,
            "issues": list(status.get("issues", [])),
            "keyCount": len(keys) if isinstance(keys, list) else 0,
            "stateDigest": status.get("stateDigest"),
        },
    )


def _catalog_registry_check(
    engine: Engine,
    catalog: LicensingCatalog,
) -> DeploymentReadinessCheck:
    try:
        with Session(engine) as session:
            status = inspect_catalog_registry(session, catalog)
    except Exception:
        return _check(
            "catalog_registry",
            False,
            "catalog_registry_ready",
            "catalog_registry_unavailable",
            {"catalogReady": False},
        )
    ready = status.get("catalogReady") is True
    releases = status.get("releases")
    return _check(
        "catalog_registry",
        ready,
        "catalog_registry_ready",
        "catalog_registry_not_ready",
        {
            "activeCatalogSha256": status.get("activeCatalogSha256"),
            "activeRevision": status.get("activeRevision"),
            "catalogReady": ready,
            "issues": list(status.get("issues", [])),
            "releaseCount": len(releases) if isinstance(releases, list) else 0,
            "runtimeCatalogSha256": status.get("runtimeCatalogSha256"),
            "runtimeRevision": status.get("runtimeRevision"),
            "stateDigest": status.get("stateDigest"),
        },
    )


def _database_checks(
    engine: Engine,
    migration_path: Path,
    now: datetime,
    maximum_backlog_age: timedelta,
) -> tuple[DeploymentReadinessCheck, ...]:
    try:
        with engine.connect() as connection:
            connection.execute(text("SELECT 1"))
            table_names = set(inspect(connection).get_table_names())
            migration_context = MigrationContext.configure(connection)
            current_heads = tuple(sorted(migration_context.get_current_heads()))
    except Exception:
        return (
            _check(
                "database_connectivity",
                False,
                "database_reachable",
                "database_unavailable",
                {"reachable": False},
            ),
            _check(
                "database_schema",
                False,
                "database_schema_complete",
                "database_schema_unavailable",
                {},
            ),
            _check(
                "migration_head",
                False,
                "migration_head_current",
                "migration_head_unavailable",
                {},
            ),
            _check(
                "worker_backlog",
                False,
                "worker_backlog_healthy",
                "worker_backlog_unavailable",
                {},
            ),
        )

    checks: list[DeploymentReadinessCheck] = [
        _check(
            "database_connectivity",
            True,
            "database_reachable",
            "database_unavailable",
            {"reachable": True},
        )
    ]
    expected_tables = set(Base.metadata.tables)
    missing_tables = tuple(sorted(expected_tables.difference(table_names)))
    checks.append(
        _check(
            "database_schema",
            not missing_tables,
            "database_schema_complete",
            "database_schema_incomplete",
            {
                "expectedTableCount": len(expected_tables),
                "presentTableCount": len(expected_tables.intersection(table_names)),
                "missingTables": list(missing_tables),
            },
        )
    )
    try:
        configuration = Config(str(migration_path))
        migration_heads = tuple(
            sorted(ScriptDirectory.from_config(configuration).get_heads())
        )
    except Exception:
        migration_heads = ()
    migration_current = bool(current_heads) and current_heads == migration_heads
    checks.append(
        _check(
            "migration_head",
            migration_current,
            "migration_head_current",
            "migration_head_mismatch",
            {
                "currentHeads": list(current_heads),
                "expectedHeads": list(migration_heads),
            },
        )
    )
    checks.append(_worker_backlog_check(engine, now, maximum_backlog_age))
    return tuple(checks)


def _worker_backlog_check(
    engine: Engine,
    now: datetime,
    maximum_backlog_age: timedelta,
) -> DeploymentReadinessCheck:
    cutoff = now - maximum_backlog_age
    try:
        with Session(engine) as session:
            failed_outboxes = session.scalar(
                select(func.count()).select_from(OutboxEvent).where(
                    OutboxEvent.status == "failed"
                )
            )
            stale_outboxes = session.scalar(
                select(func.count()).select_from(OutboxEvent).where(
                    OutboxEvent.status.in_(("pending", "processing")),
                    OutboxEvent.updated_at <= cutoff,
                )
            )
            failed_deliveries = session.scalar(
                select(func.count()).select_from(NotificationDelivery).where(
                    NotificationDelivery.status == "failed"
                )
            )
            stale_deliveries = session.scalar(
                select(func.count()).select_from(NotificationDelivery).where(
                    NotificationDelivery.status.in_(("pending", "sending")),
                    NotificationDelivery.updated_at <= cutoff,
                )
            )
    except Exception:
        return _check(
            "worker_backlog",
            False,
            "worker_backlog_healthy",
            "worker_backlog_unavailable",
            {},
        )
    counts = {
        "failedOutboxEvents": int(failed_outboxes or 0),
        "staleOutboxEvents": int(stale_outboxes or 0),
        "failedNotificationDeliveries": int(failed_deliveries or 0),
        "staleNotificationDeliveries": int(stale_deliveries or 0),
        "maximumAgeSeconds": int(maximum_backlog_age.total_seconds()),
    }
    healthy = not any(
        counts[field_name]
        for field_name in (
            "failedOutboxEvents",
            "staleOutboxEvents",
            "failedNotificationDeliveries",
            "staleNotificationDeliveries",
        )
    )
    return _check(
        "worker_backlog",
        healthy,
        "worker_backlog_healthy",
        "worker_backlog_unhealthy",
        counts,
    )


def _check(
    name: str,
    condition: bool,
    success_code: str,
    failure_code: str,
    details: Mapping[str, object],
) -> DeploymentReadinessCheck:
    if not isinstance(condition, bool):
        raise TypeError("condition must be a Boolean")
    return DeploymentReadinessCheck(
        name=name,
        status=CHECK_STATUS_PASSED if condition else CHECK_STATUS_FAILED,
        code=success_code if condition else failure_code,
        details=details,
    )


def _bounded_integer(value: object, field_name: str, maximum: int) -> int:
    if isinstance(value, bool) or not isinstance(value, int):
        raise TypeError(f"{field_name} must be an integer")
    if value < 1 or value > maximum:
        raise ValueError(f"{field_name} must be between one and {maximum}")
    return value


def _utc(value: datetime) -> datetime:
    if not isinstance(value, datetime):
        raise TypeError("value must be a datetime")
    if value.tzinfo is None or value.utcoffset() is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


__all__ = [
    "ALEMBIC_CONFIGURATION_PATH",
    "DEFAULT_MAXIMUM_BACKLOG_AGE_MINUTES",
    "DEPLOYMENT_COMPONENTS",
    "DEPLOYMENT_COMPONENT_API",
    "DEPLOYMENT_COMPONENT_NOTIFICATION_WORKER",
    "DEPLOYMENT_COMPONENT_SUBSCRIPTION_WORKER",
    "DEPLOYMENT_REPORT_SCHEMA",
    "DeploymentReadinessCheck",
    "DeploymentReadinessReport",
    "inspect_deployment_readiness",
]
