# Copyright (C) 2026 GoodData Corporation

"""Repo-owned git policy config loader (`.aida/git_policy.yaml`)."""

from __future__ import annotations

from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import cast

import yaml

from aida_mcp.utils.aida_paths import get_repo_config_root


@dataclass(frozen=True, slots=True)
class CommitPolicy:
    subject_max_chars: int = 70
    ticket_enabled: bool = True
    risk_values: tuple[str, ...] = ("nonprod", "low", "high")
    risk_enabled: bool = True
    require_co_authored_by: bool = False
    template_file: str | None = None
    pre_commit_guidance: bool = False
    ticket_prefix: str = "JIRA"
    risk_prefix: str = "risk"


@dataclass(frozen=True, slots=True)
class WorkflowPolicy:
    prefer_force_with_lease: bool = True
    autosquash_unpublished_only: bool = True


@dataclass(frozen=True, slots=True)
class PrPolicy:
    title_template_file: str | None = None
    body_template_file: str | None = None


@dataclass(frozen=True, slots=True)
class EnforcementPolicy:
    mode: str = "off"


@dataclass(frozen=True, slots=True)
class GitPolicy:
    commit: CommitPolicy = CommitPolicy()
    pr: PrPolicy = PrPolicy()
    workflow: WorkflowPolicy = WorkflowPolicy()
    enforcement: EnforcementPolicy = EnforcementPolicy()


def load_git_policy(workspace_root: Path) -> GitPolicy:
    """Load `.aida/git_policy.yaml`; return defaults on missing/invalid config."""
    path = get_repo_config_root(workspace_root) / "git_policy.yaml"
    try:
        stat = path.stat()
    except FileNotFoundError:
        return GitPolicy()
    return _load_git_policy_cached(str(path), stat.st_mtime_ns, stat.st_size)


@lru_cache(maxsize=128)
def _load_git_policy_cached(path_str: str, mtime_ns: int, file_size: int) -> GitPolicy:
    _ = mtime_ns
    _ = file_size
    path = Path(path_str)

    try:
        loaded: object = yaml.safe_load(path.read_text(encoding="utf-8"))
    except Exception:
        return GitPolicy()
    if not isinstance(loaded, dict):
        return GitPolicy()
    root = cast(dict[str, object], loaded)

    section = _mapping(root.get("git_policy"), fallback={})
    commit = _parse_commit_policy(section.get("commit"))
    pr = _parse_pr_policy(section.get("pr"))
    workflow = _parse_workflow_policy(section.get("workflow"))
    enforcement = _parse_enforcement_policy(section.get("enforcement"))
    return GitPolicy(commit=commit, pr=pr, workflow=workflow, enforcement=enforcement)


def _parse_commit_policy(raw: object) -> CommitPolicy:
    section = _mapping(raw, fallback={})
    subject_max = _int_in_range(section.get("subject_max_chars"), fallback=70, low=10, high=200)
    ticket_enabled = _bool(section.get("ticket_enabled"), fallback=True)
    risks = _risk_values(section.get("risk_values"))
    risk_enabled = _bool(section.get("risk_enabled"), fallback=True)
    require_co_authored_by = _bool(section.get("require_co_authored_by"), fallback=False)
    template_file = _optional_non_empty_str(section.get("template_file"))
    pre_commit_guidance = _bool(section.get("pre_commit_guidance"), fallback=False)
    ticket_prefix = _trailer_prefix(section.get("ticket_prefix"), fallback="JIRA")
    risk_prefix = _trailer_prefix(section.get("risk_prefix"), fallback="risk")
    return CommitPolicy(
        subject_max_chars=subject_max,
        ticket_enabled=ticket_enabled,
        risk_values=risks,
        risk_enabled=risk_enabled,
        require_co_authored_by=require_co_authored_by,
        template_file=template_file,
        pre_commit_guidance=pre_commit_guidance,
        ticket_prefix=ticket_prefix,
        risk_prefix=risk_prefix,
    )


def _parse_pr_policy(raw: object) -> PrPolicy:
    section = _mapping(raw, fallback={})
    return PrPolicy(
        title_template_file=_optional_non_empty_str(section.get("title_template_file")),
        body_template_file=_optional_non_empty_str(section.get("body_template_file")),
    )


def _parse_enforcement_policy(raw: object) -> EnforcementPolicy:
    section = _mapping(raw, fallback={})
    mode_raw = _optional_non_empty_str(section.get("mode"))
    mode = (mode_raw or "off").lower()
    if mode not in {"off", "advisory", "strict"}:
        mode = "off"
    return EnforcementPolicy(mode=mode)


def _parse_workflow_policy(raw: object) -> WorkflowPolicy:
    section = _mapping(raw, fallback={})
    prefer_lease = _bool(section.get("prefer_force_with_lease"), fallback=True)
    autosquash = _bool(section.get("autosquash_unpublished_only"), fallback=True)
    return WorkflowPolicy(prefer_force_with_lease=prefer_lease, autosquash_unpublished_only=autosquash)


def _risk_values(raw: object) -> tuple[str, ...]:
    if not isinstance(raw, list):
        return ("nonprod", "low", "high")
    values: list[str] = []
    for item in cast(list[object], raw):
        if isinstance(item, str):
            value = item.strip().lower()
            if value and value not in values:
                values.append(value)
    if not values:
        return ("nonprod", "low", "high")
    return tuple(values)


def _mapping(raw: object, *, fallback: dict[str, object]) -> dict[str, object]:
    if isinstance(raw, dict):
        return cast(dict[str, object], raw)
    return fallback


def _bool(raw: object, *, fallback: bool) -> bool:
    if isinstance(raw, bool):
        return raw
    return fallback


def _optional_non_empty_str(raw: object) -> str | None:
    if isinstance(raw, str):
        value = raw.strip()
        if value:
            return value
    return None


def _trailer_prefix(raw: object, *, fallback: str) -> str:
    value = _optional_non_empty_str(raw)
    if not value:
        return fallback
    if ":" in value:
        return fallback
    return value


def _int_in_range(raw: object, *, fallback: int, low: int, high: int) -> int:
    if isinstance(raw, bool):
        return fallback
    if isinstance(raw, int) and low <= raw <= high:
        return raw
    return fallback
