#!/usr/bin/env python3
"""Validate AItrackwalker offline deployment inputs without contacting a cluster."""

from __future__ import annotations

import argparse
import ipaddress
import re
import sys
from pathlib import Path
from typing import Any

import yaml


UNSAFE_MARKERS = ("CHANGE_ME", "UNRESOLVED", "replace-with", "minioadmin")
DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
DNS_LABEL_PATTERN = re.compile(r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")


class ValidationResult:
    def __init__(self) -> None:
        self.errors: list[str] = []
        self.warnings: list[str] = []

    def require(self, condition: bool, message: str) -> None:
        if not condition:
            self.errors.append(message)

    def warn(self, condition: bool, message: str) -> None:
        if not condition:
            self.warnings.append(message)


def load_yaml(path: Path) -> dict[str, Any]:
    try:
        content = yaml.safe_load(path.read_text(encoding="utf-8"))
    except FileNotFoundError as exc:
        raise ValueError(f"文件不存在: {path}") from exc
    except yaml.YAMLError as exc:
        raise ValueError(f"YAML 无法解析: {path}: {exc}") from exc
    if not isinstance(content, dict):
        raise ValueError(f"YAML 顶层必须是对象: {path}")
    return content


def nested(data: dict[str, Any], dotted_path: str, default: Any = None) -> Any:
    value: Any = data
    for part in dotted_path.split("."):
        if not isinstance(value, dict) or part not in value:
            return default
        value = value[part]
    return value


def hosts_in_group(inventory: dict[str, Any], group: str) -> dict[str, Any]:
    hosts = nested(inventory, f"all.children.{group}.hosts", {})
    return hosts if isinstance(hosts, dict) else {}


def has_unsafe_marker(value: Any) -> bool:
    if not isinstance(value, str):
        return False
    lowered = value.lower()
    return any(marker.lower() in lowered for marker in UNSAFE_MARKERS)


def validate_site(site: dict[str, Any], result: ValidationResult) -> str:
    result.require(site.get("schemaVersion") == 1, "site.schemaVersion 必须为 1")
    profile = nested(site, "deployment.profile")
    result.require(profile in {"single", "ha"}, "deployment.profile 必须是 single 或 ha")
    namespace = nested(site, "deployment.namespace", "")
    result.require(
        isinstance(namespace, str) and bool(DNS_LABEL_PATTERN.fullmatch(namespace)),
        "deployment.namespace 不是合法的 Kubernetes namespace",
    )
    result.require(
        nested(site, "deployment.runtimeProfile") in {"cpu-local", "server-gpu"},
        "deployment.runtimeProfile 必须是 cpu-local 或 server-gpu",
    )
    result.require(
        nested(site, "deployment.runtimeNetworkMode") in {"isolated", "restricted-egress"},
        "deployment.runtimeNetworkMode 必须是 isolated 或 restricted-egress",
    )

    networks: list[ipaddress._BaseNetwork] = []
    for key in ("cluster.podCidr", "cluster.serviceCidr"):
        value = nested(site, key)
        try:
            networks.append(ipaddress.ip_network(value, strict=False))
        except (TypeError, ValueError):
            result.errors.append(f"{key} 不是合法 CIDR: {value!r}")
    if len(networks) == 2:
        result.require(not networks[0].overlaps(networks[1]), "Pod CIDR 与 Service CIDR 不能重叠")

    data_dir = nested(site, "cluster.dataDir", "")
    result.require(isinstance(data_dir, str) and data_dir.startswith("/"), "cluster.dataDir 必须是绝对路径")
    result.require(
        nested(site, "storage.provider") in {"local-path", "longhorn", "external"},
        "storage.provider 必须是 local-path、longhorn 或 external",
    )
    result.require(
        nested(site, "storage.modelAccessMode") in {"ReadWriteOnce", "ReadWriteMany"},
        "storage.modelAccessMode 必须是 ReadWriteOnce 或 ReadWriteMany",
    )

    runtime_profile = nested(site, "deployment.runtimeProfile")
    gpu_enabled = nested(site, "gpu.enabled")
    if runtime_profile == "server-gpu":
        result.require(gpu_enabled is True, "server-gpu 档案必须启用 gpu.enabled")
    if nested(site, "addons.gpuOperator.enabled"):
        result.require(gpu_enabled is True, "启用 GPU Operator 时必须设置 gpu.enabled=true")
    if nested(site, "storage.provider") == "longhorn":
        result.require(
            nested(site, "addons.longhorn.enabled") is True,
            "storage.provider=longhorn 时必须启用 Longhorn addon",
        )
    return str(profile)


def validate_inventory(
    inventory: dict[str, Any], profile: str, site: dict[str, Any], result: ValidationResult
) -> None:
    servers = hosts_in_group(inventory, "rke2_servers")
    agents = hosts_in_group(inventory, "rke2_agents")
    gpu_workers = hosts_in_group(inventory, "gpu_workers")

    result.require(bool(servers), "inventory 至少需要一个 rke2_servers 节点")
    overlap = set(servers).intersection(agents)
    result.require(not overlap, f"节点不能同时属于 rke2_servers 和 rke2_agents: {sorted(overlap)}")
    all_cluster_hosts = {**servers, **agents}
    result.require(
        set(gpu_workers).issubset(all_cluster_hosts),
        "gpu_workers 中的节点必须同时属于 rke2_servers 或 rke2_agents",
    )

    node_ips: list[str] = []
    for hostname, variables in all_cluster_hosts.items():
        result.require(
            isinstance(variables, dict) and bool(variables.get("rke2_node_ip")),
            f"节点 {hostname} 缺少 rke2_node_ip",
        )
        node_ip = variables.get("rke2_node_ip") if isinstance(variables, dict) else None
        if node_ip:
            try:
                ipaddress.ip_address(node_ip)
                node_ips.append(str(node_ip))
            except ValueError:
                result.errors.append(f"节点 {hostname} 的 rke2_node_ip 非法: {node_ip}")
    result.require(len(node_ips) == len(set(node_ips)), "rke2_node_ip 必须唯一")

    if profile == "single":
        result.require(len(servers) == 1, "single 档案必须且只能有一个 rke2 server")
        result.require(not agents, "single 档案不应配置独立 rke2 agent")
    elif profile == "ha":
        result.require(len(servers) >= 3, "ha 档案至少需要三个 rke2 server")
        result.require(len(servers) % 2 == 1, "ha 档案的 rke2 server 数量必须为奇数")
        result.require(
            nested(site, "storage.provider") != "local-path",
            "ha 档案不能使用 local-path 作为生产存储",
        )
        result.require(
            nested(site, "storage.modelAccessMode") == "ReadWriteMany",
            "ha 档案的模型卷必须为 ReadWriteMany",
        )
        result.require(
            nested(site, "helmValues.bundledDataServices.enabled") is False,
            "ha 档案必须关闭 Chart 内置单实例数据服务",
        )

    if nested(site, "gpu.enabled") and nested(site, "gpu.requiredOnGpuWorkers", True):
        result.require(bool(gpu_workers), "启用 GPU 后 inventory 至少需要一个 gpu_workers 节点")


def validate_secrets(secrets: dict[str, Any], result: ValidationResult) -> None:
    result.require(secrets.get("schemaVersion") == 1, "secrets.schemaVersion 必须为 1")
    required_lengths = {
        "rke2.token": 32,
        "application.postgresPassword": 16,
        "application.redisPassword": 16,
        "application.minioRootUser": 3,
        "application.minioRootPassword": 16,
        "application.artifactInstallerToken": 32,
        "application.uavAccessInternalToken": 32,
        "application.flighthubEventCallbackToken": 32,
        "application.emqxDashboardUsername": 3,
        "application.emqxDashboardPassword": 16,
    }
    for key, minimum_length in required_lengths.items():
        value = nested(secrets, key)
        result.require(isinstance(value, str) and len(value) >= minimum_length, f"{key} 长度必须至少为 {minimum_length}")
        result.require(not has_unsafe_marker(value), f"{key} 仍包含默认值或占位符")
        if isinstance(value, str):
            result.require("\n" not in value and "\r" not in value, f"{key} 不能包含换行")


def validate_versions(versions: dict[str, Any], release_mode: bool, result: ValidationResult) -> None:
    result.require(versions.get("schemaVersion") == 1, "versions.schemaVersion 必须为 1")
    validated = versions.get("validated") is True
    if release_mode:
        result.require(validated, "正式发布要求 versions.lock.yaml 的 validated=true")
    else:
        result.warn(validated, "版本锁尚未通过 M0 验证，仅允许开发部署")

    for key, value in versions.items():
        if key == "images" or not isinstance(value, (dict, list)):
            continue
    serialized = yaml.safe_dump(versions, allow_unicode=True)
    if release_mode:
        result.require(
            nested(versions, "compliance.approved") is True,
            "formal release requires compliance.approved=true",
        )
        result.require("UNRESOLVED" not in serialized, "正式发布的版本锁不能包含 UNRESOLVED")

    images = versions.get("images", {})
    result.require(isinstance(images, dict) and bool(images), "versions.images 不能为空")
    if not isinstance(images, dict):
        return
    for name, image in images.items():
        if not isinstance(image, dict):
            result.errors.append(f"versions.images.{name} 必须是对象")
            continue
        reference = image.get("reference", "")
        digest = image.get("digest", "")
        result.require(bool(reference), f"镜像 {name} 缺少 reference")
        result.require(":latest" not in str(reference), f"镜像 {name} 禁止使用 latest")
        if release_mode:
            result.require(bool(DIGEST_PATTERN.fullmatch(str(digest))), f"镜像 {name} 缺少合法 sha256 digest")


def validate_cross_file(site: dict[str, Any], secrets: dict[str, Any], result: ValidationResult) -> None:
    flighthub_enabled = nested(site, "helmValues.flighthub.enabled") is True
    mqtt_enabled = nested(site, "helmValues.flighthub.mqttEnabled") is True
    if mqtt_enabled:
        result.require(flighthub_enabled, "启用 FlightHub MQTT 前必须启用 FlightHub")
        broker = nested(site, "helmValues.endpoints.mqtt.brokerUrl", "")
        result.require(str(broker).startswith(("tcp://", "ssl://")), "MQTT brokerUrl 必须使用 tcp:// 或 ssl://")
    if flighthub_enabled:
        for key in ("baseUrl", "organizationUuid", "projectUuid"):
            result.require(
                bool(nested(site, f"helmValues.flighthub.{key}")),
                f"helmValues.flighthub.{key} is required when FlightHub is enabled",
            )
        result.require(
            bool(nested(secrets, "application.flighthubUserToken")),
            "启用 FlightHub 时必须提供 application.flighthubUserToken",
        )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("--site", type=Path, required=True)
    parser.add_argument("--inventory", type=Path, required=True)
    parser.add_argument("--secrets", type=Path, required=True)
    parser.add_argument("--versions", type=Path, required=True)
    parser.add_argument("--release", action="store_true", help="启用正式发布严格校验")
    return parser.parse_args()


def main() -> int:
    args = parse_args()
    try:
        site = load_yaml(args.site)
        inventory = load_yaml(args.inventory)
        secrets = load_yaml(args.secrets)
        versions = load_yaml(args.versions)
    except ValueError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 2

    if "sops" in secrets:
        print("ERROR: validate_config.py 需要已解密的秘密文件", file=sys.stderr)
        return 2

    result = ValidationResult()
    profile = validate_site(site, result)
    validate_inventory(inventory, profile, site, result)
    validate_secrets(secrets, result)
    validate_versions(versions, args.release, result)
    validate_cross_file(site, secrets, result)

    for warning in result.warnings:
        print(f"WARNING: {warning}")
    for error in result.errors:
        print(f"ERROR: {error}", file=sys.stderr)

    if result.errors:
        print(f"配置预检失败：{len(result.errors)} 个错误，{len(result.warnings)} 个警告", file=sys.stderr)
        return 1
    print(f"配置预检通过：{len(result.warnings)} 个警告")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
