from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any

from sqlalchemy.engine import URL, make_url
from sqlalchemy.exc import ArgumentError

DEFAULT_POSTGRESQL_PORT = 5432
DISPOSABLE_TARGET_MARKERS = ("migration-probe", "disposable-probe")
SENSITIVE_QUERY_KEY_PARTS = (
    "credential",
    "passfile",
    "password",
    "secret",
    "token",
)
FORBIDDEN_ROUTING_QUERY_KEYS = frozenset(
    {
        "database",
        "dbname",
        "host",
        "hostaddr",
        "options",
        "port",
        "service",
        "servicefile",
        "user",
        "username",
    }
)


class MaintenanceDatabaseTargetError(ValueError):
    """维护命令数据库目标不满足安全约束。"""

    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code


@dataclass(frozen=True, slots=True)
class MaintenanceDatabaseTarget:
    host: str
    port: int
    database: str
    username: str | None
    sanitized_url: str
    exact_target: str
    is_disposable: bool


def _raise_target_error(code: str, message: str) -> None:
    raise MaintenanceDatabaseTargetError(code, message)


def _normalize_host(value: str) -> str:
    normalized = str(value or "").strip().lower()
    if normalized.startswith("[") and normalized.endswith("]"):
        normalized = normalized[1:-1]
    return normalized.rstrip(".")


def _normalize_probe_component(value: str) -> str:
    return re.sub(r"[^a-z0-9]+", "-", str(value or "").lower()).strip("-")


def _matches_disposable_marker(value: str) -> bool:
    normalized = _normalize_probe_component(value)
    return any(
        normalized == marker
        or normalized.startswith(f"{marker}-")
        or normalized.startswith(f"x-financial-{marker}-")
        for marker in DISPOSABLE_TARGET_MARKERS
    )


def is_disposable_maintenance_target(*, host: str, database: str) -> bool:
    """只有主机名和数据库名都带显式 probe 标记时才视为一次性目标。"""

    return _matches_disposable_marker(host) and _matches_disposable_marker(database)


def _sanitize_query(url: URL) -> URL:
    sanitized_query: dict[str, Any] = {}
    for key, value in url.query.items():
        normalized_key = str(key).lower()
        if any(part in normalized_key for part in SENSITIVE_QUERY_KEY_PARTS):
            if isinstance(value, tuple):
                sanitized_query[key] = tuple("***" for _ in value)
            else:
                sanitized_query[key] = "***"
            continue
        sanitized_query[key] = value
    return url.set(query=sanitized_query)


def _format_target_host(host: str) -> str:
    return f"[{host}]" if ":" in host else host


def parse_maintenance_database_target(
    database_url: str,
    *,
    expected_host: str,
    expected_database: str,
) -> MaintenanceDatabaseTarget:
    """解析显式数据库 URL，并核对操作人声明的目标主机和数据库。"""

    raw_url = str(database_url or "").strip()
    if not raw_url:
        _raise_target_error(
            "database_url_required",
            "维护命令必须显式提供 DATABASE_URL，禁止回退到环境文件或默认配置。",
        )

    try:
        parsed_url = make_url(raw_url)
    except (ArgumentError, TypeError, ValueError) as exc:
        raise MaintenanceDatabaseTargetError(
            "invalid_database_url",
            "DATABASE_URL 不是有效的 SQLAlchemy 数据库 URL。",
        ) from exc

    if parsed_url.get_backend_name() != "postgresql":
        _raise_target_error(
            "postgresql_required",
            "维护命令只允许连接 PostgreSQL 数据库。",
        )

    routing_query_keys = sorted(
        str(key).lower()
        for key in parsed_url.query
        if str(key).lower() in FORBIDDEN_ROUTING_QUERY_KEYS
    )
    if routing_query_keys:
        _raise_target_error(
            "database_routing_query_forbidden",
            f"DATABASE_URL 查询参数不得覆盖连接目标或 schema：{', '.join(routing_query_keys)}",
        )

    host = _normalize_host(parsed_url.host or "")
    if not host:
        _raise_target_error(
            "database_host_required",
            "DATABASE_URL 必须包含显式 PostgreSQL 主机名。",
        )

    database = str(parsed_url.database or "").strip()
    if not database:
        _raise_target_error(
            "database_name_required",
            "DATABASE_URL 必须包含显式数据库名。",
        )

    normalized_expected_host = _normalize_host(expected_host)
    if not normalized_expected_host:
        _raise_target_error(
            "expected_host_required",
            "必须通过 expected_host 声明预期数据库主机。",
        )
    if host != normalized_expected_host:
        _raise_target_error(
            "expected_host_mismatch",
            f"DATABASE_URL 主机与预期不一致：actual={host}; expected={normalized_expected_host}",
        )

    normalized_expected_database = str(expected_database or "").strip()
    if not normalized_expected_database:
        _raise_target_error(
            "expected_database_required",
            "必须通过 expected_database 声明预期数据库名。",
        )
    if database != normalized_expected_database:
        _raise_target_error(
            "expected_database_mismatch",
            "DATABASE_URL 数据库名与预期不一致："
            f"actual={database}; expected={normalized_expected_database}",
        )

    port = int(parsed_url.port or DEFAULT_POSTGRESQL_PORT)
    exact_target = f"{_format_target_host(host)}:{port}/{database}"
    sanitized_url = _sanitize_query(parsed_url).render_as_string(hide_password=True)

    return MaintenanceDatabaseTarget(
        host=host,
        port=port,
        database=database,
        username=parsed_url.username,
        sanitized_url=sanitized_url,
        exact_target=exact_target,
        is_disposable=is_disposable_maintenance_target(host=host, database=database),
    )


def validate_maintenance_database_target(
    database_url: str,
    *,
    expected_host: str,
    expected_database: str,
    apply: bool = False,
    allow_non_disposable: bool = False,
    confirm_target: str | None = None,
) -> MaintenanceDatabaseTarget:
    """校验维护目标；非一次性数据库 apply 必须显式放行并精确确认目标。"""

    target = parse_maintenance_database_target(
        database_url,
        expected_host=expected_host,
        expected_database=expected_database,
    )
    if not apply:
        return target

    normalized_confirmation = str(confirm_target or "").strip()
    if target.is_disposable:
        if normalized_confirmation and normalized_confirmation != target.exact_target:
            _raise_target_error(
                "confirm_target_mismatch",
                "confirm_target 与解析后的数据库目标不一致："
                f"actual={normalized_confirmation}; expected={target.exact_target}",
            )
        return target

    if not allow_non_disposable:
        _raise_target_error(
            "non_disposable_apply_forbidden",
            "非一次性数据库 apply 必须显式启用 allow_non_disposable。",
        )
    if normalized_confirmation != target.exact_target:
        _raise_target_error(
            "confirm_target_mismatch",
            "非一次性数据库 apply 必须通过 confirm_target 精确确认："
            f"expected={target.exact_target}",
        )
    return target
