"""First-run initialization, validation, and factory reset for the vault."""

from __future__ import annotations

import importlib.resources
import logging
import re
import shutil
from pathlib import Path

import tomli_w
import yaml

log = logging.getLogger(__name__)

# Default config template
_DEFAULT_CONFIG = {
    "llm": {
        "mode": "cloud",
        "cloud_provider": "anthropic",
        "cloud_model": "claude-sonnet-4-20250514",
        "local_provider": "ollama",
        "local_model": "llama3",
    },
    "server": {
        "host": "127.0.0.1",
        "port": 8000,
    },
    "indexing": {
        "exclude_dirs": [],
        # Default ingest output language policy.
        # Options: "source" | "user" | "english" | "<iso-639-1 code>"
        # "source" = summaries match the source language (recommended)
        "language_policy": "source",
    },
}

# Directory structure — two zones:
#   root_dir/agntspace-*  User-facing folders (inbox, journal, knowledge, projects)
#   root_dir/agntspace/   Agent internals (system, memory)

# Directories INSIDE agntspace/ (agent internals)
_INTERNAL_DIRS = [  # arch:deterministic — fixed vault internal directory structure created on first-run init; architectural layout, not tunable
    "system/identity",       # SOUL, USER, PROFILE, CONTRACT, TRUST
    "system/agents",         # Subagent definitions
    "system/skills",         # Skill definitions (SKILL.md files)
    "system/security",       # Blacklist, audit trail
    "system/logs",           # Server logs
    "system/logs/simulation",
    "memory",                # Long-term memory + knowledge graph
    "memory/pending",        # Pending memory updates (awaiting approval)
    "memory/graph",          # SPO knowledge graph
    "memory/summaries",      # Layered temporal summaries
]

# Directories at ROOT level (user-facing, agntspace-* prefixed)
_ROOT_DIRS = [  # arch:deterministic — fixed top-level vault layout (the five agntspace-* siblings + agntspace/ internals); structural invariant, never per-user
    "agntspace-inbox",       # Drop zone
    "agntspace-journal",     # User journal + agent observations
    "agntspace-knowledge",   # Knowledge bases
    "agntspace-projects",    # Projects (tasks live inside {slug}/tasks/)
    "agntspace-goals",       # Goals (top-level, many-to-many with projects)
]


def _find_defaults_dir() -> Path:
    """Locate the defaults/ directory."""
    repo_defaults = Path(__file__).parent.parent.parent.parent.parent / "defaults"
    if repo_defaults.is_dir():
        return repo_defaults
    try:
        ref = importlib.resources.files("agntspace") / "defaults"
        pkg_path = Path(str(ref))
        if pkg_path.is_dir():
            return pkg_path
    except (TypeError, FileNotFoundError):
        pass
    raise FileNotFoundError("Cannot find defaults/ directory.")


def _copy_defaults(source: Path, dest: Path) -> None:
    """Copy default files, skipping any that already exist."""
    for src_file in source.rglob("*"):
        if src_file.name == ".gitkeep":
            continue
        if not src_file.is_file():
            continue
        relative = src_file.relative_to(source)
        dst_file = dest / relative
        if dst_file.exists():
            continue
        dst_file.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src_file, dst_file)
        log.debug("Copied: %s", relative)


def run_init(vault_dir: Path, *, minimal: bool = False) -> Path:
    """Initialize AgntSpace inside a root directory.

    Creates:
      root_dir/agntspace-inbox/       (drop zone)
      root_dir/agntspace-journal/     (user journal + agent observations)
      root_dir/agntspace-knowledge/   (knowledge bases)
      root_dir/agntspace-projects/    (projects; tasks live inside each project)
      root_dir/agntspace-goals/       (goals — top-level, many-to-many with projects)
      root_dir/agntspace/             (agent internals: system + memory)

    The root_dir can be any folder — Obsidian vault, Dropbox, plain folder.
    User's existing content is never touched.
    """
    root = vault_dir
    vault = root / "agntspace"

    # Create root-level user-facing directories
    for dirname in _ROOT_DIRS:
        (root / dirname).mkdir(parents=True, exist_ok=True)

    # Create internal directories inside agntspace/
    for sub in _INTERNAL_DIRS:
        (vault / sub).mkdir(parents=True, exist_ok=True)

    # Copy default vault files (skip if vault already initialized)
    identity_dir = vault / "system" / "identity"
    vault_exists = (identity_dir / "SOUL.md").exists() and (identity_dir / "CONTRACT.md").exists()
    try:
        defaults = _find_defaults_dir()
    except FileNotFoundError:
        if vault_exists:
            log.debug("Defaults dir not found but vault already initialized — skipping copy")
            defaults = None
        else:
            raise
    defaults_vault = defaults / "vault" if defaults else None
    if defaults_vault and defaults_vault.is_dir():
        # Identity files → agntspace/system/identity/
        for identity_file in ["SOUL.md", "USER.md", "PROFILE.md", "CONTRACT.md", "TRUST.md"]:
            src = defaults_vault / identity_file
            dst = vault / "system" / "identity" / identity_file
            if src.exists() and not dst.exists():
                shutil.copy2(src, dst)
                log.debug("Copied identity: %s", identity_file)

        # MEMORY.md → agntspace/memory/
        memory_src = defaults_vault / "memory" / "MEMORY.md"
        memory_dst = vault / "memory" / "MEMORY.md"
        if memory_src.exists() and not memory_dst.exists():
            shutil.copy2(memory_src, memory_dst)
            log.debug("Copied memory: MEMORY.md")

        # RELATIONSHIP.md → agntspace/system/identity/
        rel_src = defaults_vault / "RELATIONSHIP.md"
        rel_dst = vault / "system" / "identity" / "RELATIONSHIP.md"
        if rel_src.exists() and not rel_dst.exists():
            shutil.copy2(rel_src, rel_dst)
            log.debug("Copied identity: RELATIONSHIP.md")

        # Agent definitions → agntspace/system/agents/
        agents_src = defaults_vault / "agents"
        if agents_src.is_dir():
            _copy_defaults(agents_src, vault / "system" / "agents")

        # Security defaults → agntspace/system/security/
        security_src = defaults_vault / "security"
        if security_src.is_dir():
            _copy_defaults(security_src, vault / "system" / "security")

    # Skills → agntspace/system/skills/
    if defaults:
        defaults_skills = defaults / "skills"
        skills_dir = vault / "system" / "skills"
        if defaults_skills.is_dir():
            _copy_defaults(defaults_skills, skills_dir)
            log.debug("Copied default skills to %s", skills_dir)

    # Write config.toml to app home (~/.agntspace/) — not to external vault
    home_vault = Path.home() / ".agntspace"
    home_vault.mkdir(parents=True, exist_ok=True)
    config_path = home_vault / "config.toml"
    if not config_path.exists():
        config_path.write_bytes(tomli_w.dumps(_DEFAULT_CONFIG).encode())
        log.info("Created %s", config_path)

    # If this is an Obsidian vault and agntspace/ was previously excluded,
    # remove the exclusion — we WANT Obsidian to see agntspace files so
    # [[wikilinks]] appear in Obsidian's graph view. The two systems share
    # the same link graph intentionally.
    obsidian_config = root / ".obsidian" / "app.json"
    if obsidian_config.exists():
        try:
            import json as _json
            cfg = _json.loads(obsidian_config.read_text(encoding="utf-8"))
            excluded = cfg.get("userIgnoreFilters", [])
            vault_name = vault.name  # "agntspace"
            removed = False
            for pattern in [vault_name, f"{vault_name}/"]:
                if pattern in excluded:
                    excluded.remove(pattern)
                    removed = True
            if removed:
                cfg["userIgnoreFilters"] = excluded
                obsidian_config.write_text(_json.dumps(cfg, indent=2), encoding="utf-8")
                log.info("Removed %s/ from Obsidian excluded folders — wikilinks now shared", vault_name)
        except Exception:
            log.debug("Could not update Obsidian config", exc_info=True)

    log.debug("AgntSpace initialized at %s (vault: %s)", root, vault)
    return root


# ---- Vault Health Validation ----

# Map of system files: filename → vault-relative path
_IDENTITY_FILES = {  # arch:deterministic — structural filename→vault-path mapping for the five identity files; the files themselves are templates the user edits
    "SOUL.md": "system/identity/SOUL.md",
    "USER.md": "system/identity/USER.md",
    "PROFILE.md": "system/identity/PROFILE.md",
    "CONTRACT.md": "system/identity/CONTRACT.md",
    "TRUST.md": "system/identity/TRUST.md",
}

# Reset categories
# Categories available for reset. Memory excluded by default — it's the agent's
# accumulated knowledge about the user, expensive to rebuild.
RESET_CATEGORIES = ("identity", "agents", "skills", "security")
RESET_CATEGORIES_ALL = ("identity", "memory", "agents", "skills", "security")


def _resolve_vault(vault_dir: Path) -> Path:
    """Resolve to the agntspace directory."""
    vault = vault_dir / "agntspace" if vault_dir.name != "agntspace" else vault_dir
    if not vault.is_dir():
        raise FileNotFoundError(f"Vault not found: {vault}")
    return vault


def validate_vault(vault_dir: Path) -> dict[str, dict]:
    """Validate system files and return per-file health status.

    Returns dict keyed by display name, each with:
      status: "ok" | "warning" | "error"
      message: human-readable description
      path: vault-relative path
    """
    vault = _resolve_vault(vault_dir)
    results: dict[str, dict] = {}

    # --- Identity files ---
    for filename, vault_path in _IDENTITY_FILES.items():
        full = vault / vault_path
        name = filename.replace(".md", "")

        if not full.is_file():
            results[name] = {"status": "error", "message": f"{filename} is missing", "path": vault_path}
            continue

        text = full.read_text(encoding="utf-8")

        # Check for unfilled placeholders
        if "[placeholder]" in text.lower() or "[your name]" in text.lower():
            results[name] = {"status": "warning", "message": f"{filename} has unfilled placeholders", "path": vault_path}
            continue

        # File-specific validation
        if filename == "CONTRACT.md":
            results[name] = _validate_contract(text, vault_path)
        elif filename == "TRUST.md":
            results[name] = _validate_trust(text, vault_path)
        else:
            results[name] = {"status": "ok", "message": f"{filename} looks good", "path": vault_path}

    # --- MEMORY.md ---
    mem_full = vault / "memory" / "MEMORY.md"
    if not mem_full.is_file():
        results["MEMORY"] = {"status": "error", "message": "MEMORY.md is missing", "path": "memory/MEMORY.md"}
    else:
        results["MEMORY"] = {"status": "ok", "message": "MEMORY.md looks good", "path": "memory/MEMORY.md"}

    # --- Agents ---
    agents_dir = vault / "system" / "agents"
    agent_count = len(list(agents_dir.glob("*.md"))) if agents_dir.is_dir() else 0
    if agent_count == 0:
        results["Agents"] = {"status": "warning", "message": "No agent definitions found", "path": "system/agents/"}
    else:
        results["Agents"] = {"status": "ok", "message": f"{agent_count} agent definitions", "path": "system/agents/"}

    # --- Skills ---
    skills_dir = vault / "system" / "skills"
    skill_count = len(list(skills_dir.rglob("SKILL.md"))) if skills_dir.is_dir() else 0
    if skill_count == 0:
        results["Skills"] = {"status": "warning", "message": "No skills found", "path": "system/skills/"}
    else:
        results["Skills"] = {"status": "ok", "message": f"{skill_count} skills loaded", "path": "system/skills/"}

    return results


def _validate_contract(text: str, vault_path: str) -> dict:
    """Validate CONTRACT.md YAML block."""
    # Extract YAML block
    match = re.search(r"```ya?ml\s*\n(.*?)```", text, re.DOTALL)
    if not match:
        return {"status": "error", "message": "CONTRACT.md has no YAML permissions block", "path": vault_path}

    try:
        data = yaml.safe_load(match.group(1))
    except yaml.YAMLError as exc:
        return {"status": "error", "message": f"CONTRACT.md YAML is malformed: {exc}", "path": vault_path}

    if not isinstance(data, dict):
        return {"status": "error", "message": "CONTRACT.md YAML block is not a valid mapping", "path": vault_path}

    issues = []
    if "default" not in data:
        issues.append("missing 'default' field")
    if not data.get("allow") and not data.get("confirm"):
        issues.append("no actions in allow or confirm")

    if issues:
        return {"status": "warning", "message": f"CONTRACT.md: {', '.join(issues)}", "path": vault_path}

    return {"status": "ok", "message": "CONTRACT.md looks good", "path": vault_path}


def _validate_trust(text: str, vault_path: str) -> dict:
    """Validate TRUST.md table format."""
    valid_rows = 0
    for line in text.splitlines():
        line = line.strip()
        if not line.startswith("|"):
            continue
        cells = [c.strip() for c in line.split("|")[1:-1]]
        if len(cells) >= 6 and cells[0] and not cells[0].startswith("-"):
            # Skip header row
            if cells[0].lower() == "domain":
                continue
            valid_rows += 1

    if valid_rows == 0:
        return {"status": "warning", "message": "TRUST.md has no valid trust score rows", "path": vault_path}

    return {"status": "ok", "message": f"TRUST.md: {valid_rows} domains tracked", "path": vault_path}


# ---- Factory Reset ----


def reset_vault(vault_dir: Path, categories: list[str] | None = None) -> dict:
    """Reset vault system files to factory defaults.

    Args:
        vault_dir: Root directory containing agntspace/.
        categories: Which categories to reset. None = all.
            Valid: "identity", "memory", "agents", "skills", "security".

    User content (journal, inbox, knowledge, projects, goals) is never touched.

    Returns a summary of what was reset.
    """
    vault = _resolve_vault(vault_dir)
    cats = set(categories) if categories else set(RESET_CATEGORIES)
    defaults = _find_defaults_dir()
    defaults_vault = defaults / "vault"
    reset_files: list[str] = []

    if "identity" in cats and defaults_vault.is_dir():
        for identity_file in ["SOUL.md", "USER.md", "PROFILE.md", "CONTRACT.md", "TRUST.md"]:
            src = defaults_vault / identity_file
            dst = vault / "system" / "identity" / identity_file
            if src.exists():
                dst.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(src, dst)
                reset_files.append(f"system/identity/{identity_file}")

    if "memory" in cats:
        # Reset MEMORY.md
        if defaults_vault.is_dir():
            mem_src = defaults_vault / "memory" / "MEMORY.md"
            mem_dst = vault / "memory" / "MEMORY.md"
            if mem_src.exists():
                mem_dst.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(mem_src, mem_dst)
                reset_files.append("memory/MEMORY.md")
        # Clear graph files
        for graph_file in ("memory/graph/entities.json", "memory/graph/triples.json"):
            gf = vault / graph_file
            if gf.exists():
                gf.unlink()
                reset_files.append(graph_file)
        # Clear pending memory
        pending = vault / "memory" / "pending"
        if pending.is_dir():
            for f in pending.glob("*.md"):
                f.unlink()
                reset_files.append(f"memory/pending/{f.name}")

    if "agents" in cats and defaults_vault.is_dir():
        agents_src = defaults_vault / "agents"
        if agents_src.is_dir():
            _overwrite_defaults(agents_src, vault / "system" / "agents")
            reset_files.append("system/agents/*")

    if "skills" in cats:
        defaults_skills = defaults / "skills"
        if defaults_skills.is_dir():
            _overwrite_defaults(defaults_skills, vault / "system" / "skills")
            reset_files.append("system/skills/*")

    if "security" in cats and defaults_vault.is_dir():
        security_src = defaults_vault / "security"
        if security_src.is_dir():
            _overwrite_defaults(security_src, vault / "system" / "security")
            reset_files.append("system/security/*")

    log.info("Vault reset (%s): %d items restored", ", ".join(cats), len(reset_files))
    return {"reset_files": reset_files, "count": len(reset_files), "categories": sorted(cats)}


def reset_file(vault_dir: Path, vault_path: str) -> dict:
    """Reset a single system file to its factory default.

    Args:
        vault_dir: Root directory containing agntspace/.
        vault_path: Vault-relative path (e.g., "identity/CONTRACT.md").

    Returns summary with the reset file path.
    """
    vault = _resolve_vault(vault_dir)
    defaults = _find_defaults_dir()
    defaults_vault = defaults / "vault"

    # Map vault paths to their default source
    source_map = {
        "system/identity/SOUL.md": defaults_vault / "SOUL.md",
        "system/identity/USER.md": defaults_vault / "USER.md",
        "system/identity/PROFILE.md": defaults_vault / "PROFILE.md",
        "system/identity/CONTRACT.md": defaults_vault / "CONTRACT.md",
        "system/identity/TRUST.md": defaults_vault / "TRUST.md",
        "memory/MEMORY.md": defaults_vault / "memory" / "MEMORY.md",
    }

    src = source_map.get(vault_path)
    if not src or not src.exists():
        raise FileNotFoundError(f"No default exists for: {vault_path}")

    dst = vault / vault_path
    dst.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dst)
    log.info("Reset single file: %s", vault_path)
    return {"reset_file": vault_path}


def _overwrite_defaults(source: Path, dest: Path) -> None:
    """Copy default files, overwriting existing ones."""
    for src_file in source.rglob("*"):
        if src_file.name == ".gitkeep":
            continue
        if not src_file.is_file():
            continue
        relative = src_file.relative_to(source)
        dst_file = dest / relative
        dst_file.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src_file, dst_file)
        log.debug("Reset: %s", relative)
