# Copyright (C) 2026 GoodData Corporation

"""Explicit semantic auto-fixes for repo-owned AIDA configs."""

from __future__ import annotations

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

import yaml

from aida_mcp.utils.aida_paths import get_repo_config_root
from aida_mcp.utils.repo_config_audit import RepoConfigAuditFinding, audit_loaded_repo_configs

_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 auto_fix_repo_configs(*, workspace_root: Path, dry_run: bool = False) -> list[str]:
    config_root = get_repo_config_root(workspace_root)
    if not config_root.exists():
        return []

    original_texts: dict[str, str] = {}
    loaded_files: dict[str, dict[str, object]] = {}
    for filename in (
        "change_domains.yaml",
        "validation_policy.yaml",
        "validation_registry.yaml",
        "git_policy.yaml",
        "rules_selection.yaml",
    ):
        path = config_root / filename
        if not path.exists():
            loaded_files[filename] = {}
            continue
        original_texts[filename] = path.read_text(encoding="utf-8")
        loaded_files[filename] = _load_mapping_from_text(original_texts[filename])

    changes: list[str] = []
    while True:
        findings = [
            finding
            for finding in audit_loaded_repo_configs(
                config_root=config_root,
                change_domains_root=loaded_files["change_domains.yaml"],
                validation_policy_root=loaded_files["validation_policy.yaml"],
                validation_registry_root=loaded_files["validation_registry.yaml"],
                git_policy_root=loaded_files["git_policy.yaml"],
                rules_selection_root=loaded_files["rules_selection.yaml"],
            )
            if finding.fixable and finding.subject
        ]
        if not findings:
            break

        grouped: dict[str, list[RepoConfigAuditFinding]] = {}
        for finding in findings:
            grouped.setdefault(finding.file, []).append(finding)

        iteration_changes: list[str] = []
        for filename, file_findings in grouped.items():
            path = config_root / filename
            if not path.exists():
                continue
            data = loaded_files[filename]
            file_changes: list[str] = []
            if filename == "validation_policy.yaml":
                file_changes.extend(_apply_validation_policy_fixes(data=data, findings=file_findings))
            elif filename == "validation_registry.yaml":
                file_changes.extend(_apply_validation_registry_fixes(data=data, findings=file_findings))
            elif filename == "change_domains.yaml":
                file_changes.extend(_apply_change_domains_fixes(data=data, findings=file_findings))
            iteration_changes.extend(file_changes)
            if file_changes and not dry_run:
                _write_mapping_with_header(path=path, original_text=original_texts[filename], data=data)
        if not iteration_changes:
            break
        changes.extend(iteration_changes)
    return changes


def _apply_validation_policy_fixes(*, data: dict[str, object], findings: list[RepoConfigAuditFinding]) -> list[str]:
    changes: list[str] = []
    root = _mapping(data.get("validation_policy"))
    pipelines = _mapping(root.get("pipelines"))
    for finding in findings:
        if finding.code != "unused_pipeline" or finding.subject is None:
            continue
        if finding.subject in pipelines:
            pipelines.pop(finding.subject, None)
            changes.append(
                f".aida/validation_policy.yaml: removed unused pipeline '{finding.subject}' via doctor --auto-fix"
            )
    root["pipelines"] = pipelines
    data["validation_policy"] = root
    return changes


def _apply_validation_registry_fixes(*, data: dict[str, object], findings: list[RepoConfigAuditFinding]) -> list[str]:
    changes: list[str] = []
    root = _mapping(data.get("registry"))
    commands = _mapping(root.get("commands"))
    processors = _mapping(root.get("processors"))
    for finding in findings:
        if finding.subject is None:
            continue
        if finding.code == "unused_command" and finding.subject in commands:
            commands.pop(finding.subject, None)
            changes.append(
                f".aida/validation_registry.yaml: removed unused command '{finding.subject}' via doctor --auto-fix"
            )
        if finding.code == "unused_processor" and finding.subject in processors:
            processors.pop(finding.subject, None)
            changes.append(
                f".aida/validation_registry.yaml: removed unused processor '{finding.subject}' via doctor --auto-fix"
            )
    root["commands"] = commands
    root["processors"] = processors
    data["registry"] = root
    return changes


def _apply_change_domains_fixes(*, data: dict[str, object], findings: list[RepoConfigAuditFinding]) -> list[str]:
    changes: list[str] = []
    domains_any = data.get("domains")
    if not isinstance(domains_any, list):
        return changes
    domains = cast(list[object], domains_any)
    unused_ids = {finding.subject for finding in findings if finding.code == "unused_domain" and finding.subject}
    if not unused_ids:
        return changes
    kept: list[object] = []
    removed_ids: set[str] = set()
    for item in domains:
        if not isinstance(item, Mapping):
            kept.append(item)
            continue
        mapping = _mapping(cast(object, item))
        domain_id = mapping.get("id")
        if isinstance(domain_id, str) and domain_id in unused_ids:
            removed_ids.add(domain_id)
            continue
        kept.append(item)
    data["domains"] = kept
    for domain_id in sorted(removed_ids):
        changes.append(f".aida/change_domains.yaml: removed unused domain '{domain_id}' via doctor --auto-fix")
    return changes


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_mapping_from_text(text: str) -> dict[str, object]:
    try:
        loaded = yaml.safe_load(text)
    except Exception:
        return {}
    return _mapping(loaded)


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()}
