#!/usr/bin/env python3
"""Validate safe runner configuration for disposable 1C extension validation."""

from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Any


ALLOWED_RUNNER_KINDS = {"manual", "designer_cli", "onescript", "custom"}
ALLOWED_VALIDATION_MODES = {"manual", "load_and_syntax_check", "load_syntax_and_smoke"}
FORBIDDEN_BASE_MARKERS = {"prod", "production", "рабоч", "боев", "real", "main"}
SECRET_KEY_RE = re.compile(r"(password|passwd|pwd|secret|token|ключ|парол)", re.IGNORECASE)
CONNECTION_SECRET_RE = re.compile(r"(pwd|password|usr|user)\s*=", re.IGNORECASE)


def load_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8-sig"))


def write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def issue(severity: str, code: str, message: str, *, path: Path | str | None = None, detail: dict[str, Any] | None = None) -> dict[str, Any]:
    result: dict[str, Any] = {"severity": severity, "code": code, "message": message}
    if path is not None:
        result["path"] = str(path)
    if detail:
        result["detail"] = detail
    return result


def find_secret_keys(value: Any, prefix: str = "") -> list[str]:
    hits: list[str] = []
    if isinstance(value, dict):
        for key, nested in value.items():
            current = f"{prefix}.{key}" if prefix else str(key)
            if SECRET_KEY_RE.search(str(key)):
                hits.append(current)
            hits.extend(find_secret_keys(nested, current))
    elif isinstance(value, list):
        for index, nested in enumerate(value):
            hits.extend(find_secret_keys(nested, f"{prefix}[{index}]"))
    return hits


def string_contains_forbidden_marker(value: str) -> str | None:
    lowered = value.casefold()
    for marker in FORBIDDEN_BASE_MARKERS:
        if marker in lowered:
            return marker
    return None


def sanitized_config(config: dict[str, Any]) -> dict[str, Any]:
    allowed = {
        "schema",
        "runner_id",
        "runner_kind",
        "platform_version",
        "platform_bin",
        "disposable_base_ref",
        "disposable_base_kind",
        "disposable_base_confirmed",
        "validation_mode",
        "evidence_root",
        "notes",
    }
    return {key: value for key, value in config.items() if key in allowed}


def check_config(config_path: Path) -> dict[str, Any]:
    findings: list[dict[str, Any]] = []
    if not config_path.exists() or not config_path.is_file():
        findings.append(issue("error", "missing_runner_config", "Runner config file is missing.", path=config_path))
        return build_result(config_path, None, findings)

    config = load_json(config_path)
    if config.get("schema") != "onec_extension_runner_config.v1":
        findings.append(issue("error", "invalid_runner_config_schema", "Runner config schema must be onec_extension_runner_config.v1.", path=config_path, detail={"schema": config.get("schema")}))

    runner_id = config.get("runner_id")
    if not isinstance(runner_id, str) or not runner_id.strip():
        findings.append(issue("error", "missing_runner_id", "runner_id is required.", path=config_path))

    runner_kind = config.get("runner_kind")
    if runner_kind not in ALLOWED_RUNNER_KINDS:
        findings.append(issue("error", "invalid_runner_kind", "runner_kind is not supported.", path=config_path, detail={"allowed": sorted(ALLOWED_RUNNER_KINDS), "actual": runner_kind}))

    validation_mode = config.get("validation_mode")
    if validation_mode not in ALLOWED_VALIDATION_MODES:
        findings.append(issue("error", "invalid_validation_mode", "validation_mode is not supported.", path=config_path, detail={"allowed": sorted(ALLOWED_VALIDATION_MODES), "actual": validation_mode}))

    disposable_base_ref = config.get("disposable_base_ref")
    if not isinstance(disposable_base_ref, str) or not disposable_base_ref.strip():
        findings.append(issue("error", "missing_disposable_base_ref", "disposable_base_ref is required.", path=config_path))
    else:
        marker = string_contains_forbidden_marker(disposable_base_ref)
        if marker:
            findings.append(issue("error", "production_like_base_ref", "disposable_base_ref contains a production-like marker.", path=config_path, detail={"marker": marker}))
        if CONNECTION_SECRET_RE.search(disposable_base_ref):
            findings.append(issue("error", "secret_in_disposable_base_ref", "disposable_base_ref must not contain user/password connection data.", path=config_path))

    if config.get("disposable_base_confirmed") is not True:
        findings.append(issue("error", "disposable_base_not_confirmed", "disposable_base_confirmed must be true.", path=config_path))

    platform_bin = config.get("platform_bin")
    if platform_bin is not None:
        if not isinstance(platform_bin, str) or not platform_bin.strip():
            findings.append(issue("error", "invalid_platform_bin", "platform_bin must be a non-empty string when provided.", path=config_path))
        elif runner_kind in {"designer_cli", "custom"} and not Path(platform_bin).exists():
            findings.append(issue("warning", "platform_bin_not_found", "platform_bin does not exist on this machine; runner may be remote or not installed here.", path=platform_bin))

    evidence_root = config.get("evidence_root")
    if evidence_root is not None and (not isinstance(evidence_root, str) or not evidence_root.strip()):
        findings.append(issue("error", "invalid_evidence_root", "evidence_root must be a non-empty string when provided.", path=config_path))

    secret_keys = find_secret_keys(config)
    for key in secret_keys:
        findings.append(issue("error", "secret_key_in_runner_config", "Runner config must not contain secrets or credentials.", path=config_path, detail={"key": key}))

    unknown = sorted(set(config) - set(sanitized_config(config)))
    for key in unknown:
        findings.append(issue("warning", "unknown_runner_config_key", "Unknown runner config key will be ignored by the adapter.", path=config_path, detail={"key": key}))

    return build_result(config_path, sanitized_config(config), findings)


def build_result(config_path: Path, config: dict[str, Any] | None, findings: list[dict[str, Any]]) -> dict[str, Any]:
    errors = [row for row in findings if row.get("severity") == "error"]
    warnings = [row for row in findings if row.get("severity") == "warning"]
    return {
        "schema": "onec_extension_runner_config_check.v1",
        "config_path": str(config_path),
        "config_schema": (config or {}).get("schema"),
        "passed": not errors,
        "sanitized_config": config,
        "findings": findings,
        "counts": {
            "errors": len(errors),
            "warnings": len(warnings),
        },
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate safe runner configuration for disposable 1C extension validation.")
    parser.add_argument("--config", type=Path, required=True)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    result = check_config(args.config)
    if args.output:
        write_json(args.output, result)
    print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "counts": result["counts"]}, ensure_ascii=False))
    return 0 if result["passed"] else 2


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