"""config_loader — Schicht-2-Loader für den Drei-Schichten-Generalisierungs-Standard.

Präzedenz: skill.config.yaml > bundle.json vault_layout > DEFAULT_LAYOUT.
Zeit-Platzhalter ({YYYY-MM}, {DD}, {HHMM}) werden NICHT aufgelöst — das ist
Aufgabe des jeweiligen Skills zur Laufzeit.
"""
from __future__ import annotations

import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml

# Neun geschlossene Layout-Schlüssel. Erweiterung dieser Liste ist eine
# Bundle-API-Änderung und betrifft alle Skills, die paths-Placeholder nutzen.
ALLOWED_PLACEHOLDERS: set[str] = {
    "logbuch", "projects", "context", "system",
    "areas", "resources", "archive", "attachments", "inbox",
}

DEFAULT_LAYOUT: dict[str, str] = {
    "logbuch":     "05 Logbuch",
    "projects":    "02 Projekte",
    "context":     "00 Kontext",
    "system":      "08 Systemsteuerung",
    "areas":       "03 Bereiche",
    "resources":   "04 Ressourcen",
    "archive":     "06 Archiv",
    "attachments": "07 Anhänge",
    "inbox":       "01 Inbox",
}

# Platzhalter-Muster: {wort} — aber nicht {YYYY-MM}, {DD}, {HHMM} (Großbuchstaben/Zahl)
_LAYOUT_PLACEHOLDER = re.compile(r"\{([a-z][a-z_]*)\}")


@dataclass
class SkillConfig:
    paths: dict[str, Path] = field(default_factory=dict)
    layout: dict[str, Path] = field(default_factory=dict)  # alle 9 Wurzeln — immer gefüllt
    frontmatter: dict[str, Any] = field(default_factory=dict)
    features: dict[str, bool] = field(default_factory=dict)
    locale: str = "de"
    raw: dict = field(default_factory=dict)


def _find_bundle_root(start: Path) -> Path | None:
    """Sucht bundle.json aufwärts vom start-Verzeichnis."""
    current = start.resolve()
    for _ in range(10):
        if (current / "bundle.json").is_file():
            return current
        parent = current.parent
        if parent == current:
            break
        current = parent
    return None


def _is_safe_layout_value(v) -> bool:
    """True wenn v ein valider relativer Pfad-String ohne Traversal ist.

    Geblockt werden:
    - Absolute Pfade laut Path.is_absolute() (Windows: C:\\..., UNC \\\\...)
    - POSIX-artige absolute Pfade mit führendem "/" (auf Windows nicht von
      is_absolute() erkannt, aber auf einem echten Vault dennoch gefährlich)
    - Path-Traversal via ".." in den Pfad-Teilen
    """
    if not isinstance(v, str) or not v.strip():
        return False
    p = Path(v)
    if p.is_absolute():
        return False
    # POSIX-absolute Pfade wie "/abs/path" werden auf Windows nicht von
    # is_absolute() erkannt — explizit prüfen.
    if v.startswith("/"):
        return False
    if ".." in p.parts:
        return False
    return True


def _load_vault_layout(bundle_root: Path | None) -> dict[str, str]:
    """Liest vault_layout aus bundle.json. Fehlt die Datei → DEFAULT_LAYOUT.

    Unsichere Werte (absolute Pfade oder Path-Traversal) werden mit einer
    stderr-Warnung abgelehnt; der Default-Wert für den betroffenen Key greift.
    """
    if bundle_root is None:
        return DEFAULT_LAYOUT.copy()
    bundle_path = bundle_root / "bundle.json"
    if not bundle_path.is_file():
        return DEFAULT_LAYOUT.copy()
    data = json.loads(bundle_path.read_text(encoding="utf-8"))
    layout = data.get("vault_layout")
    if not isinstance(layout, dict):
        return DEFAULT_LAYOUT.copy()
    merged = DEFAULT_LAYOUT.copy()
    for k, v in layout.items():
        if not isinstance(k, str) or k not in ALLOWED_PLACEHOLDERS:
            continue
        if not _is_safe_layout_value(v):
            print(
                f"config_loader: bundle.json vault_layout['{k}'] = {v!r} ist unsicher "
                f"(absolut oder Path-Traversal). Fallback auf Default '{DEFAULT_LAYOUT[k]}'.",
                file=sys.stderr,
            )
            continue
        merged[k] = v
    return merged


def _resolve_placeholders(raw_str: str, layout: dict[str, str]) -> str:
    """Ersetzt {logbuch}, {projects} etc. in raw_str. Unbekannte → ValueError."""
    def replace(m: re.Match) -> str:
        key = m.group(1)
        if key not in ALLOWED_PLACEHOLDERS:
            raise ValueError(
                f"Unbekannter Layout-Platzhalter {{{key!s}}} in Pfad-String '{raw_str}'. "
                f"Erlaubt: {sorted(ALLOWED_PLACEHOLDERS)}"
            )
        return layout[key]

    return _LAYOUT_PLACEHOLDER.sub(replace, raw_str)


def _resolve_paths(
    raw_paths: dict,
    layout: dict[str, str],
    vault_root: Path | None,
) -> dict[str, Path]:
    """Wendet Platzhalter-Auflösung auf alle Pfad-Strings an.

    Hinweis Sicherheit: Im Gegensatz zu `_load_vault_layout` wird hier KEIN
    Path-Traversal-Schutz angewandt. Skill-Pfade kommen aus `skill.config.yaml`
    und können legitim `..` enthalten (z.B. Navigation zwischen Geschwister-
    Bereichen). Wenn das Bundle für öffentliche Distribution geöffnet wird
    und externe Skills geladen werden können, muss der Setup-Agent diese
    Werte zusätzlich gegen ein erlaubtes Präfix prüfen.
    """
    result: dict[str, Path] = {}
    for key, val in raw_paths.items():
        if not isinstance(val, str):
            result[key] = Path(str(val))
            continue
        resolved_str = _resolve_placeholders(val, layout)
        p = Path(resolved_str)
        if vault_root is not None:
            p = vault_root / p
        result[key] = p
    return result


def load_skill_config(
    skill_dir: Path,
    vault_root: Path | None = None,
    bundle_root: Path | None = None,
    default_paths: dict[str, str] | None = None,
) -> SkillConfig:
    """Lädt skill.config.yaml im skill_dir (optional — fehlt = leeres Config).

    1. skill.config.yaml lesen. Fehlt → leeres Config.
    2. bundle.json laden und vault_layout ziehen. Fehlt → DEFAULT_LAYOUT.
    3. Pfad-Strings mit Layout-Platzhaltern ersetzen.
    4. Wenn vault_root übergeben → Pfade absolut auflösen.
    5. Unbekannte Platzhalter → ValueError.

    Skill-eigene Defaults: über `default_paths` kann der Skill seine
    Fallback-Pfade mit Layout-Platzhaltern angeben (z.B.
    `{"output_dir": "{logbuch}/Z Session Logs/{YYYY-MM}/{DD}"}`). Pro
    Schlüssel gilt: skill.config.yaml > default_paths > nichts. Die
    Default-Pfade werden mit demselben Layout aufgelöst wie die
    Config-Pfade, sodass `bundle.json.vault_layout`-Overrides auch
    ohne skill.config.yaml greifen.
    """
    config_file = skill_dir / "skill.config.yaml"

    raw: dict = {}
    if config_file.is_file():
        loaded = yaml.safe_load(config_file.read_text(encoding="utf-8"))
        if isinstance(loaded, dict):
            raw = loaded

    # Bundle-Root ermitteln
    if bundle_root is None:
        bundle_root = _find_bundle_root(skill_dir)

    layout = _load_vault_layout(bundle_root)

    # Alle 9 Layout-Wurzeln vorauflösen — garantiert vollständige layout_paths.
    # Iteration über ALLOWED_PLACEHOLDERS (set): Reihenfolge egal, Zugriff per Key.
    # Unbekannte Extra-Keys aus bundle.json.vault_layout fallen hier still raus.
    layout_paths: dict[str, Path] = {}
    for key in ALLOWED_PLACEHOLDERS:
        p = Path(layout[key])
        if vault_root is not None:
            p = vault_root / p
        layout_paths[key] = p

    raw_paths = raw.get("paths", {}) if isinstance(raw.get("paths", {}), dict) else {}
    # Skill-eigene Defaults nur dort einsetzen, wo die Config nichts sagt.
    if default_paths:
        merged_paths = {**default_paths, **raw_paths}
    else:
        merged_paths = raw_paths
    paths = _resolve_paths(merged_paths, layout, vault_root)

    frontmatter: dict[str, Any] = raw.get("frontmatter", {})
    if not isinstance(frontmatter, dict):
        frontmatter = {}

    features: dict[str, bool] = raw.get("features", {})
    if not isinstance(features, dict):
        features = {}

    locale: str = raw.get("locale", "de")
    if not isinstance(locale, str):
        locale = "de"

    return SkillConfig(
        paths=paths,
        layout=layout_paths,
        frontmatter=frontmatter,
        features=features,
        locale=locale,
        raw=raw,
    )
