# Copyright (C) 2026 GoodData Corporation

"""Materialize rules from multiple sources into a single directory.

Why:
- Existing retrieval code expects a single `rules_dir` on disk.
- Our target architecture has multiple rule sources:
  - embedded rules shipped with the AIDA package
  - repo-owned rules under `.aida/rules/`

This module builds an on-disk "overlay" directory in the user cache and returns its path.
Later sources override earlier ones on path collisions.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from importlib.resources.abc import Traversable
from pathlib import Path
from typing import TYPE_CHECKING

from aida_mcp.tools.rules.selection import compute_selected_rule_paths, load_rules_selection_config
from aida_mcp.utils.aida_paths import DOT_AIDA
from aida_mcp.utils.artifacts import get_artifacts_root

if TYPE_CHECKING:
    from collections.abc import Iterable, Iterator


@dataclass(frozen=True, slots=True)
class RuleText:
    rel_path: str
    content: str
    origin: str
    editable: bool


def _iter_files(root: Path) -> Iterator[RuleText]:
    if not root.exists():
        return
    for p in root.rglob("*.mdc"):
        try:
            content = p.read_text(encoding="utf-8")
        except OSError:
            continue
        rel = str(p.relative_to(root))
        yield RuleText(rel_path=rel, content=content, origin="repo", editable=True)


def _iter_embedded(package: str) -> Iterator[RuleText]:
    """Iterate *.mdc files from an embedded rules package."""
    try:
        import importlib.resources as resources
    except Exception:
        return

    try:
        root = resources.files(package)
    except ModuleNotFoundError:
        return

    root_t: Traversable = root

    def walk(node: Traversable, prefix: str) -> Iterator[RuleText]:
        if node.is_dir():
            for child in node.iterdir():
                yield from walk(child, f"{prefix}{child.name}/")
            return

        if node.name.endswith(".mdc"):
            try:
                content = node.read_text(encoding="utf-8")
            except Exception:
                return
            yield RuleText(rel_path=prefix.rstrip("/"), content=content, origin="embedded", editable=False)

    for child in root_t.iterdir():
        yield from walk(child, f"{child.name}/")


def _dedupe_in_precedence_order(items: Iterable[RuleText]) -> list[RuleText]:
    """Last write wins per rel_path (deterministic)."""
    by_path: dict[str, RuleText] = {}
    for it in items:
        by_path[it.rel_path] = it
    return [by_path[k] for k in sorted(by_path.keys())]


def materialize_rules_dir(*, workspace_root: Path) -> Path:
    """Materialize rule sources into a single on-disk directory and return it."""
    embedded_items: list[RuleText] = list(_iter_embedded("aida_mcp.embedded_rules"))
    repo_dir = workspace_root / DOT_AIDA / "rules"
    repo_items = list(_iter_files(repo_dir))

    selection = load_rules_selection_config(workspace_root=workspace_root)
    selected = compute_selected_rule_paths(
        embedded_paths={item.rel_path for item in embedded_items},
        repo_paths={item.rel_path for item in repo_items},
        config=selection,
    )

    selected_embedded_paths = set(selected.embedded_paths)
    selected_repo_paths = set(selected.repo_paths)
    embedded_selected = [item for item in embedded_items if item.rel_path in selected_embedded_paths]
    repo_selected = [item for item in repo_items if item.rel_path in selected_repo_paths]

    # Precedence (lowest -> highest): embedded -> repo-owned.
    items = _dedupe_in_precedence_order([*embedded_selected, *repo_selected])

    # Compute stable key from paths + content.
    h = hashlib.sha256()
    for it in items:
        h.update(it.rel_path.encode("utf-8"))
        h.update(b"\0")
        h.update(it.content.encode("utf-8"))
        h.update(b"\0")
    key = h.hexdigest()[:16]

    out_root = get_artifacts_root() / "rules" / "materialized" / key
    out_root.mkdir(parents=True, exist_ok=True)

    for it in items:
        dst = out_root / it.rel_path
        dst.parent.mkdir(parents=True, exist_ok=True)
        # Write unconditionally: this keeps the directory consistent even if partially populated.
        dst.write_text(it.content, encoding="utf-8")

    sources_manifest = {
        it.rel_path: {
            "origin": it.origin,
            "editable": it.editable,
        }
        for it in items
    }
    (out_root / ".aida-rule-sources.json").write_text(
        json.dumps(sources_manifest, sort_keys=True),
        encoding="utf-8",
    )

    return out_root
