# Copyright (C) 2026 GoodData Corporation

"""AIDA preferences config loader (`.aida/preferences.yaml` + `.aida/preferences.local.yaml`)."""

from __future__ import annotations

import logging
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

logger = logging.getLogger(__name__)

PREFERENCES_CONFIG_NAME = "preferences.yaml"
LOCAL_PREFERENCES_CONFIG_NAME = "preferences.local.yaml"

_VALID_MODES = {"full", "native"}
_VALID_VALIDATION_VALUES = {"auto", "ask", "disabled"}
_VALID_COMMIT_VALUES = {"auto", "ask", "disabled"}
_VALID_PR_VALUES = {"auto", "ask", "disabled"}


@dataclass(frozen=True, slots=True)
class ValidationPreferences:
    pre_commit: str = "auto"
    pre_push: str = "auto"


@dataclass(frozen=True, slots=True)
class CommitPreferences:
    mode: str = "auto"


@dataclass(frozen=True, slots=True)
class PrPreferences:
    mode: str = "auto"


@dataclass(frozen=True, slots=True)
class AidaPreferences:
    mode: str = "full"
    validation: ValidationPreferences = ValidationPreferences()
    commit: CommitPreferences = CommitPreferences()
    pr: PrPreferences = PrPreferences()


@dataclass(frozen=True, slots=True)
class _PartialPreferences:
    mode: str | None = None
    validation_pre_commit: str | None = None
    validation_pre_push: str | None = None
    commit_mode: str | None = None
    pr_mode: str | None = None


def load_preferences(*, workspace_root: Path) -> AidaPreferences:
    """Load `.aida/preferences.yaml` merged with `.aida/preferences.local.yaml`.

    Local values override shared.  Missing/invalid files gracefully fall back to defaults.
    """
    config_root = get_repo_config_root(workspace_root)
    shared_path = config_root / PREFERENCES_CONFIG_NAME
    local_path = config_root / LOCAL_PREFERENCES_CONFIG_NAME

    shared = _load_partial(shared_path)
    local = _load_partial(local_path)

    return AidaPreferences(
        mode=_first_valid(local.mode, shared.mode, valid=_VALID_MODES, fallback="full"),
        validation=ValidationPreferences(
            pre_commit=_first_valid(
                local.validation_pre_commit,
                shared.validation_pre_commit,
                valid=_VALID_VALIDATION_VALUES,
                fallback="auto",
            ),
            pre_push=_first_valid(
                local.validation_pre_push,
                shared.validation_pre_push,
                valid=_VALID_VALIDATION_VALUES,
                fallback="auto",
            ),
        ),
        commit=CommitPreferences(
            mode=_first_valid(local.commit_mode, shared.commit_mode, valid=_VALID_COMMIT_VALUES, fallback="auto"),
        ),
        pr=PrPreferences(
            mode=_first_valid(local.pr_mode, shared.pr_mode, valid=_VALID_PR_VALUES, fallback="auto"),
        ),
    )


def _first_valid(*values: str | None, valid: set[str], fallback: str) -> str:
    """Return the first non-None value that is in the valid set, or fallback."""
    for v in values:
        if v is not None and v in valid:
            return v
    return fallback


def _load_partial(path: Path) -> _PartialPreferences:
    if not path.exists():
        return _PartialPreferences()
    try:
        stat = path.stat()
    except OSError:
        return _PartialPreferences()
    return _load_partial_cached(str(path), stat.st_mtime_ns, stat.st_size)


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

    try:
        loaded: object = yaml.safe_load(path.read_text(encoding="utf-8"))
    except Exception as exc:
        logger.warning("Failed to read preferences config %s: %s", path, exc)
        return _PartialPreferences()
    if not isinstance(loaded, dict):
        logger.warning("Preferences config %s must be a mapping; ignoring.", path)
        return _PartialPreferences()

    obj = cast(dict[str, object], loaded)
    section = _mapping(obj.get("preferences"), fallback={})

    mode = _optional_str(section.get("mode"))

    validation = _mapping(section.get("validation"), fallback={})
    validation_pre_commit = _optional_str(validation.get("pre_commit"))
    validation_pre_push = _optional_str(validation.get("pre_push"))

    commit = _mapping(section.get("commit"), fallback={})
    commit_mode = _optional_str(commit.get("mode"))

    pr = _mapping(section.get("pr"), fallback={})
    pr_mode = _optional_str(pr.get("mode"))

    return _PartialPreferences(
        mode=mode,
        validation_pre_commit=validation_pre_commit,
        validation_pre_push=validation_pre_push,
        commit_mode=commit_mode,
        pr_mode=pr_mode,
    )


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


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