# Copyright (C) 2026 GoodData Corporation

"""Conservative reconciliation for repo-owned AIDA YAML configs."""

from __future__ import annotations

from collections.abc import Mapping
from copy import deepcopy
from pathlib import Path
from typing import cast

import yaml

from aida_mcp.server.repo_onboarding_templates import (
    placeholder_change_domains_yaml,
    placeholder_git_policy_yaml,
    placeholder_rules_selection_yaml,
    placeholder_validation_policy_yaml,
    placeholder_validation_registry_yaml,
)
from aida_mcp.utils.aida_paths import get_repo_config_root

_QUOTED_SCALARS = {"on", "off", "yes", "no", "true", "false", "null"}


class _RepoConfigDumper(yaml.SafeDumper):
    pass


def _represent_str(dumper: yaml.SafeDumper, data: str) -> yaml.nodes.ScalarNode:
    style = '"' if data.lower() in _QUOTED_SCALARS else None
    return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)


_RepoConfigDumper.add_representer(str, _represent_str)


def reconcile_repo_configs(*, workspace_root: Path, dry_run: bool) -> list[str]:
    config_root = get_repo_config_root(workspace_root)
    if not config_root.exists():
        return []

    updates: list[str] = []
    updates.extend(_reconcile_git_policy(config_root=config_root, dry_run=dry_run))
    updates.extend(
        _reconcile_defaults_file(
            config_root=config_root,
            filename="change_domains.yaml",
            defaults=_load_default_mapping(placeholder_change_domains_yaml()),
            required_root_key=None,
            dry_run=dry_run,
        )
    )
    updates.extend(
        _reconcile_defaults_file(
            config_root=config_root,
            filename="validation_policy.yaml",
            defaults=_load_default_mapping(placeholder_validation_policy_yaml()),
            required_root_key="validation_policy",
            dry_run=dry_run,
        )
    )
    updates.extend(
        _reconcile_defaults_file(
            config_root=config_root,
            filename="validation_registry.yaml",
            defaults=_load_default_mapping(placeholder_validation_registry_yaml()),
            required_root_key="registry",
            dry_run=dry_run,
        )
    )
    updates.extend(
        _reconcile_defaults_file(
            config_root=config_root,
            filename="rules_selection.yaml",
            defaults=_load_default_mapping(placeholder_rules_selection_yaml(embedded_include_paths=[])),
            required_root_key=None,
            dry_run=dry_run,
        )
    )
    return updates


def _reconcile_git_policy(*, config_root: Path, dry_run: bool) -> list[str]:
    path = config_root / "git_policy.yaml"
    if not path.exists():
        return []

    original_text = path.read_text(encoding="utf-8")
    current = _load_mapping_from_text(original_text)
    updated = deepcopy(current)
    changes: list[str] = []

    root = _ensure_mapping(updated, "git_policy")
    commit = _ensure_mapping(root, "commit")
    workflow = _ensure_mapping(root, "workflow")
    pr = _ensure_mapping(root, "pr")
    enforcement = _ensure_mapping(root, "enforcement")

    defaults = _load_default_mapping(placeholder_git_policy_yaml())

    obsolete_keys = (
        "require_ai_share",
        "enable_ai_share_auto",
        "preferred_command",
    )
    for key in obsolete_keys:
        if key in commit:
            commit.pop(key, None)
            changes.append(f".aida/git_policy.yaml: removed obsolete key git_policy.commit.{key}")

    for key in (
        "subject_max_chars",
        "ticket_enabled",
        "risk_values",
        "risk_enabled",
        "require_co_authored_by",
        "template_file",
        "pre_commit_guidance",
        "ticket_prefix",
        "risk_prefix",
    ):
        if key in commit and _git_policy_value_invalid(path=("commit", key), value=commit[key]):
            commit.pop(key, None)
            changes.append(f".aida/git_policy.yaml: removed invalid key git_policy.commit.{key}")

    for key in ("prefer_force_with_lease", "autosquash_unpublished_only"):
        if key in workflow and not isinstance(workflow[key], bool):
            workflow.pop(key, None)
            changes.append(f".aida/git_policy.yaml: removed invalid key git_policy.workflow.{key}")

    for key in ("title_template_file", "body_template_file"):
        if key in pr and _git_policy_template_invalid(pr[key]):
            pr.pop(key, None)
            changes.append(f".aida/git_policy.yaml: removed invalid key git_policy.pr.{key}")

    if "mode" in enforcement:
        mode = enforcement["mode"]
        if not isinstance(mode, str) or mode.strip().lower() not in {"off", "advisory", "strict"}:
            enforcement.pop("mode", None)
            changes.append(".aida/git_policy.yaml: removed invalid key git_policy.enforcement.mode")

    changes.extend(_fill_missing_mapping(target=updated, defaults=defaults, file_path=".aida/git_policy.yaml"))

    if not changes:
        return []
    if not dry_run:
        _write_mapping_with_header(path=path, original_text=original_text, data=updated)
    return changes


def _reconcile_defaults_file(
    *,
    config_root: Path,
    filename: str,
    defaults: dict[str, object],
    required_root_key: str | None,
    dry_run: bool,
) -> list[str]:
    path = config_root / filename
    if not path.exists():
        return []
    original_text = path.read_text(encoding="utf-8")
    current = _load_mapping_from_text(original_text)
    updated = deepcopy(current)
    changes: list[str] = []

    if required_root_key is not None and not isinstance(updated.get(required_root_key), Mapping):
        updated[required_root_key] = deepcopy(defaults[required_root_key])
        changes.append(f".aida/{filename}: reset invalid key {required_root_key} to current default shape")

    changes.extend(_fill_missing_mapping(target=updated, defaults=defaults, file_path=f".aida/{filename}"))
    if not changes:
        return []
    if not dry_run:
        _write_mapping_with_header(path=path, original_text=original_text, data=updated)
    return changes


def _fill_missing_mapping(
    *,
    target: dict[str, object],
    defaults: dict[str, object],
    file_path: str,
    key_path: str = "",
) -> list[str]:
    changes: list[str] = []
    for key, default_value in defaults.items():
        full_key = f"{key_path}.{key}" if key_path else key
        if key not in target:
            target[key] = deepcopy(default_value)
            changes.append(f"{file_path}: added missing key {full_key}")
            continue
        if isinstance(default_value, Mapping):
            current = target.get(key)
            if not isinstance(current, Mapping):
                target[key] = deepcopy(default_value)
                changes.append(f"{file_path}: reset invalid key {full_key} to current default shape")
                continue
            nested_target = _mapping(current)
            target[key] = nested_target
            nested_changes = _fill_missing_mapping(
                target=nested_target,
                defaults=_mapping(default_value),
                file_path=file_path,
                key_path=full_key,
            )
            changes.extend(nested_changes)
    return changes


def _git_policy_template_invalid(value: object) -> bool:
    if value is None:
        return False
    return not isinstance(value, str) or not value.strip()


def _git_policy_value_invalid(*, path: tuple[str, str], value: object) -> bool:
    section, key = path
    if section != "commit":
        return False
    if key == "subject_max_chars":
        return isinstance(value, bool) or not isinstance(value, int) or not (10 <= value <= 200)
    if key == "risk_values":
        if not isinstance(value, list):
            return True
        values = [item.strip().lower() for item in value if isinstance(item, str) and item.strip()]
        return not values
    if key in {"ticket_enabled", "risk_enabled", "require_co_authored_by", "pre_commit_guidance"}:
        return not isinstance(value, bool)
    if key in {"template_file"}:
        return _git_policy_template_invalid(value)
    if key in {"ticket_prefix", "risk_prefix"}:
        return not isinstance(value, str) or not value.strip() or ":" in value
    return False


def _write_mapping_with_header(*, path: Path, original_text: str, data: dict[str, object]) -> None:
    header = _extract_header_comments(original_text)
    body = yaml.dump(data, Dumper=_RepoConfigDumper, sort_keys=False, allow_unicode=False).rstrip("\n") + "\n"
    path.write_text(f"{header}{body}" if header else body, encoding="utf-8")


def _extract_header_comments(text: str) -> str:
    lines = text.splitlines()
    kept: list[str] = []
    index = 0
    while index < len(lines):
        line = lines[index]
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            kept.append(line)
            index += 1
            continue
        break
    if not kept:
        return ""
    return "\n".join(kept).rstrip("\n") + "\n"


def _load_default_mapping(text: str) -> dict[str, object]:
    loaded = yaml.safe_load(text)
    mapping = _mapping(loaded)
    git_policy = _mapping(mapping.get("git_policy"))
    enforcement = _mapping(git_policy.get("enforcement"))
    if enforcement.get("mode") is False:
        enforcement["mode"] = "off"
        git_policy["enforcement"] = enforcement
        mapping["git_policy"] = git_policy
    return mapping


def _load_mapping_from_text(text: str) -> dict[str, object]:
    try:
        loaded = yaml.safe_load(text)
    except Exception:
        return {}
    return _mapping(loaded)


def _ensure_mapping(mapping: dict[str, object], key: str) -> dict[str, object]:
    current = mapping.get(key)
    if isinstance(current, Mapping):
        normalized = _mapping(current)
        mapping[key] = normalized
        return normalized
    normalized: dict[str, object] = {}
    mapping[key] = normalized
    return normalized


def _mapping(raw: object) -> dict[str, object]:
    if not isinstance(raw, Mapping):
        return {}
    return {str(k): v for k, v in cast(Mapping[object, object], raw).items()}
