"""Skills and SkillSchool API endpoints."""

from __future__ import annotations

from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel

router = APIRouter(prefix="/skills", tags=["skills"])


class SkillUpdateRequest(BaseModel):
    content: str


class SkillCreateRequest(BaseModel):
    name: str
    title: str
    description: str
    category: str = "private"
    triggers: list[str] = []
    instructions: str = ""
    author: str = "user"
    tools_granted: list[str] = []
    depends_on: list[str] = []
    priority: int = 0
    model_tier: str = ""
    examples: list[dict] = []
    output_format: str = ""


class SkillImportRequest(BaseModel):
    path: str
    category: str = "private"
    overwrite: bool = False


class SkillFileRequest(BaseModel):
    content: str


@router.get("")
async def list_skills(request: Request) -> list[dict]:
    """List all loaded skills."""
    skills = request.app.state.skills
    return skills.list_skills()


@router.post("/analyze")
async def analyze_patterns(request: Request) -> list[dict]:
    """Run SkillSchool pattern analysis on completed tasks."""
    skillschool = request.app.state.skillschool
    return await skillschool.analyze_patterns()


# ── Voyager-style semantic skill retrieval (2026-04-24) ──────────────

@router.get("/match")
async def match_skills(
    request: Request,
    q: str,
    top_k: int = 5,
    scope: str | None = None,
    min_score: float = 0.0,
) -> dict:
    """Find skills most semantically related to a task description.

    Query parameters:
      - ``q``: free-form task text (required)
      - ``top_k``: max matches to return (default 5, capped at 50)
      - ``scope``: optional comma-separated skill-name allowlist
      - ``min_score``: cosine similarity floor (default 0 — no filtering)

    Returns ``{query, hits: [{name, title, description, score}], fallback}``.
    When the embedding provider isn't wired (Ollama not running, or the
    embedding model isn't pulled), ``hits`` is empty and ``fallback=true``
    — callers should treat that as "retrieval unavailable, use priority
    order".
    """
    skills = request.app.state.skills

    top_k = max(1, min(50, int(top_k)))
    scope_list: list[str] | None = None
    if scope:
        scope_list = [s.strip() for s in scope.split(",") if s.strip()]

    matches = skills.find_matching_skills(
        task_text=q,
        top_k=top_k,
        scope=scope_list,
        min_score=float(min_score),
    )

    hits: list[dict] = []
    for name, score in matches:
        skill = skills.get_skill(name)
        if skill is None:
            continue
        hits.append({
            "name": skill.name,
            "title": skill.title,
            "description": skill.description,
            "score": round(float(score), 4),
            "category": skill.category,
            "priority": skill.priority,
        })

    return {
        "query": q,
        "hits": hits,
        "fallback": len(matches) == 0,
    }


# ── Hybrid skill sync (upgrades from shipped defaults) ──────────────────
#
# Registered BEFORE the catch-all GET /{skill_name} below so `/sync/*`
# paths aren't swallowed by the skill-name matcher.

def _build_skill_sync(request: Request):  # noqa: ANN202
    """Construct a SkillSync bound to the current vault + shipped defaults."""
    from agntspace.setup.init import _find_defaults_dir as _find_app_defaults
    from agntspace.skills.sync import SkillSync
    settings = request.app.state.settings
    return SkillSync(
        skills_dir=settings.skills_dir,
        defaults_dir=_find_app_defaults() / "skills",
    )


@router.get("/sync/status")
async def sync_status(request: Request) -> dict:
    """Cheap status read — counts only, no diff content."""
    try:
        sync = _build_skill_sync(request)
    except FileNotFoundError:
        return {"shipped_count": 0, "tracked": 0, "pending_count": 0, "pending_paths": []}
    return sync.status()


@router.get("/sync/pending")
async def sync_pending(request: Request) -> list[dict]:
    """List pending upgrades with full shipped+vault content for diffing."""
    try:
        sync = _build_skill_sync(request)
    except FileNotFoundError:
        return []
    return [
        {
            "path": p.path,
            "shipped_hash": p.shipped_hash,
            "accepted_hash": p.accepted_hash,
            "vault_hash": p.vault_hash,
            "shipped_content": p.shipped_content,
            "vault_content": p.vault_content,
            "classification": p.classification,
            "vault_edited": p.vault_edited,
        }
        for p in sync.list_pending()
    ]


@router.post("/sync/run")
async def sync_run(request: Request) -> dict:
    """Force an immediate sync pass (normally runs at boot)."""
    try:
        sync = _build_skill_sync(request)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc
    report = sync.sync_on_boot()
    # Reload the in-memory skill catalog so the new versions take effect
    # without a server restart.
    request.app.state.skills.load_all()
    return {
        "summary": report.summary(),
        "created": report.created,
        "overwritten_meta": report.overwritten_meta,
        "upgraded_clean": report.upgraded_clean,
        "skipped_user_edit": report.skipped_user_edit,
        "pending": report.pending,
    }


class SyncDecisionRequest(BaseModel):
    path: str


@router.post("/sync/accept")
async def sync_accept(request: Request, body: SyncDecisionRequest) -> dict:
    """User accepts the shipped version for a single pending path."""
    try:
        sync = _build_skill_sync(request)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc
    ok = sync.accept_upgrade(body.path)
    if not ok:
        raise HTTPException(status_code=404, detail=f"No pending upgrade for {body.path}")
    request.app.state.skills.load_all()
    return {"status": "accepted", "path": body.path}


@router.post("/sync/keep-mine")
async def sync_keep_mine(request: Request, body: SyncDecisionRequest) -> dict:
    """User chooses to keep their vault edit; manifest is pinned to it."""
    try:
        sync = _build_skill_sync(request)
    except FileNotFoundError as exc:
        raise HTTPException(status_code=500, detail=str(exc)) from exc
    ok = sync.keep_mine(body.path)
    if not ok:
        raise HTTPException(status_code=404, detail=f"No pending upgrade for {body.path}")
    return {"status": "kept", "path": body.path}


@router.post("/import")
async def import_skill(request: Request, body: SkillImportRequest) -> dict:
    """Import a skill from an external folder path.

    The folder must contain a SKILL.md file. The entire folder is copied
    into system/skills/{category}/{slug}/ and the skill loader is reloaded.
    """
    import re
    import shutil
    from pathlib import Path as P

    source = P(body.path).resolve()

    # --- Validate source ---
    if not source.is_dir():
        # Maybe they pointed at the SKILL.md file itself
        if source.is_file() and source.name == "SKILL.md":
            source = source.parent
        else:
            raise HTTPException(status_code=400, detail=f"Path is not a directory: {body.path}")

    skill_md = source / "SKILL.md"
    if not skill_md.is_file():
        raise HTTPException(status_code=400, detail="No SKILL.md found in the given path")

    # --- Parse SKILL.md to get the slug ---
    import frontmatter as fm

    try:
        raw = skill_md.read_text(encoding="utf-8")
        # Strip UTF-8 BOM — some editors emit it and frontmatter.loads silently
        # returns empty metadata when the leading `---` isn't at byte zero.
        if raw.startswith("\ufeff"):
            raw = raw.lstrip("\ufeff")
        post = fm.loads(raw)
    except Exception as exc:
        raise HTTPException(status_code=400, detail=f"Failed to parse SKILL.md: {exc}") from exc

    # If metadata is empty but the body still contains a `---...---` block,
    # the file likely had broken leading bytes or was previously written with
    # stacked frontmatter. Try to re-parse the body.
    if not post.metadata and "---" in post.content:
        try:
            retry = fm.loads(post.content.lstrip())
            if retry.metadata:
                post = retry
        except Exception:
            pass

    meta = post.metadata
    name = meta.get("name", source.name)
    slug = re.sub(r"[^a-z0-9-]", "", str(name).lower().replace(" ", "-"))[:60]
    if not slug:
        slug = re.sub(r"[^a-z0-9-]", "", source.name.lower().replace(" ", "-"))[:60]
    if not slug:
        raise HTTPException(status_code=400, detail="Could not derive a valid skill slug")

    # --- Check for conflicts ---
    skills = request.app.state.skills
    existing = skills.get_skill(slug)
    if existing and not body.overwrite:
        raise HTTPException(
            status_code=409,
            detail=f"Skill '{slug}' already exists at {existing.path}. Re-import with overwrite=true to replace it.",
        )
    if existing and body.overwrite:
        # Resolve the existing skill's on-disk folder and delete it before
        # copying. `existing.path` is relative to skills_dir and points at
        # the SKILL.md file — its parent is the folder we need to remove.
        settings_for_delete = request.app.state.settings
        existing_dir = (P(settings_for_delete.skills_dir) / existing.path).parent
        try:
            if existing_dir.is_dir() and existing_dir.resolve().is_relative_to(P(settings_for_delete.skills_dir).resolve()):
                shutil.rmtree(existing_dir)
        except Exception as exc:
            raise HTTPException(status_code=500, detail=f"Failed to remove existing skill: {exc}") from exc

    # --- Copy into vault ---
    settings = request.app.state.settings
    # `settings.skills_dir` already resolves to `{vault_dir}/system/skills`.
    # Previously we hand-built `vault_dir / "agntspace" / "system" / "skills"`,
    # which doubled the `agntspace` segment because `vault_dir` already ends in
    # it — skills landed outside the loader's search root and never appeared.
    dest = P(settings.skills_dir) / body.category / slug
    dest.mkdir(parents=True, exist_ok=True)

    # Copy all files from source into dest
    copied_files: list[str] = []
    for item in source.rglob("*"):
        if not item.is_file():
            continue
        rel = item.relative_to(source)
        target = dest / rel
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(item, target)
        copied_files.append(str(rel).replace("\\", "/"))

    # --- Normalize frontmatter so the card shows up active + working ---
    # Import is an explicit user trust action; force status=active, drop any
    # signature/publisher fields so the signed-skill verifier can't quarantine
    # it based on a stranger's key, and pin the category to where we filed it.
    dest_skill_md = dest / "SKILL.md"
    try:
        post.metadata["status"] = "active"
        post.metadata["category"] = body.category
        post.metadata.pop("signature", None)
        post.metadata.pop("publisher", None)
        dest_skill_md.write_text(fm.dumps(post), encoding="utf-8")
    except Exception as exc:  # pragma: no cover — frontmatter write failed
        raise HTTPException(status_code=500, detail=f"Failed to normalize SKILL.md: {exc}") from exc

    # --- Reload skill loader ---
    skills.load_all()

    loaded = skills.get_skill(slug)
    return {
        "status": "imported",
        "name": slug,
        "title": loaded.title if loaded else meta.get("title", slug),
        "category": body.category,
        "path": f"system/skills/{body.category}/{slug}/SKILL.md",
        "files_copied": len(copied_files),
        "files": copied_files,
    }


@router.delete("/{skill_name}")
async def delete_skill(request: Request, skill_name: str) -> dict:
    """Remove a skill folder from the vault and reload the loader.

    Only skills under the vault's skills_dir may be deleted — this prevents a
    path-traversal slug from reaching into defaults or anywhere else on disk.
    Skills in the shipped `_meta/` engine tier are also refused; users can
    disable them via status in frontmatter but shouldn't delete them outright.
    """
    import shutil
    from pathlib import Path as P

    skills = request.app.state.skills
    skill = skills.get_skill(skill_name)
    if not skill:
        raise HTTPException(status_code=404, detail=f"Skill not found: {skill_name}")

    if skill.category == "_meta":
        raise HTTPException(
            status_code=403,
            detail="Engine-tier (_meta) skills can't be deleted — disable via frontmatter status instead.",
        )

    settings = request.app.state.settings
    skills_root = P(settings.skills_dir).resolve()
    skill_dir = (P(settings.skills_dir) / skill.path).parent
    try:
        resolved = skill_dir.resolve()
    except OSError as exc:
        raise HTTPException(status_code=500, detail=f"Cannot resolve skill folder: {exc}") from exc

    if not resolved.is_relative_to(skills_root):
        raise HTTPException(status_code=403, detail="Skill path escapes skills directory")

    try:
        if resolved.is_dir():
            shutil.rmtree(resolved)
    except Exception as exc:
        raise HTTPException(status_code=500, detail=f"Failed to delete skill: {exc}") from exc

    skills.load_all()
    return {"status": "deleted", "name": skill_name}


@router.get("/{skill_name}")
async def get_skill(request: Request, skill_name: str) -> dict:
    """Get a single skill's full content."""
    skills = request.app.state.skills
    skill = skills.get_skill(skill_name)
    if not skill:
        raise HTTPException(status_code=404, detail=f"Skill not found: {skill_name}")
    return {
        "name": skill.name,
        "title": skill.title,
        "description": skill.description,
        "category": skill.category,
        "status": skill.status,
        "triggers": skill.triggers,
        "body": skill.body,
        "path": str(skill.path),
        "files": skill.files,
        "author": skill.author,
        "tools_granted": skill.tools_granted,
        "depends_on": skill.depends_on,
        "priority": skill.priority,
        "model_tier": skill.model_tier,
        "examples": skill.examples,
        "output_format": skill.output_format,
        "version": skill.version,
        "signature": skill.signature,
        "publisher": skill.publisher,
        "content_hash": skill.content_hash,
        "signature_valid": _signature_valid(skill),
        "verification": _verification(skill),
    }


def _signature_valid(skill) -> bool | None:  # noqa: ANN001
    """Back-compat thin wrapper over `verify_signature` returning bool|None."""
    from agntspace.skills.signing import verify_signature
    status, _ = verify_signature(
        getattr(skill, "body", "") or "",
        getattr(skill, "signature", "") or "",
    )
    if status == "valid":
        return True
    if status == "invalid":
        return False
    return None  # unsigned or unknown scheme


def _verification(skill) -> dict:  # noqa: ANN001
    """Full verification report for the skill (gap #8)."""
    from agntspace.skills.signing import skill_verification_result
    return skill_verification_result(skill)


@router.put("/{skill_name}")
async def update_skill(request: Request, skill_name: str, body: SkillUpdateRequest) -> dict:
    """Update a skill's raw content (frontmatter + body)."""
    settings = request.app.state.settings
    skills = request.app.state.skills
    skill = skills.get_skill(skill_name)
    if not skill:
        raise HTTPException(status_code=404, detail=f"Skill not found: {skill_name}")

    from agntspace.vault.writer import VaultWriter

    writer = VaultWriter(settings.vault_dir)
    writer.write(f"system/skills/{skill.path}", body.content)
    skills.load_all()
    return {"status": "updated", "name": skill_name}


@router.post("/create")
async def create_skill(request: Request, body: SkillCreateRequest) -> dict:
    """Create a new private skill (never shipped, never synced from defaults)."""
    import re

    settings = request.app.state.settings
    skills = request.app.state.skills

    slug = re.sub(r"[^a-z0-9-]", "", body.name.lower().replace(" ", "-"))[:60]
    if not slug:
        raise HTTPException(status_code=400, detail="Invalid skill name")

    # Build SKILL.md content
    triggers_yaml = "\n".join(f'  - "{t}"' for t in body.triggers) if body.triggers else '  - "' + slug + '"'
    tools_yaml = "\n".join(f'  - "{t}"' for t in body.tools_granted) if body.tools_granted else ""
    deps_yaml = "\n".join(f'  - "{d}"' for d in body.depends_on) if body.depends_on else ""

    # Build examples YAML
    examples_yaml = ""
    if body.examples:
        lines = []
        for ex in body.examples:
            lines.append(f'  - input: "{ex.get("input", "")}"')
            lines.append(f'    output: "{ex.get("output", "")}"')
        examples_yaml = "\n".join(lines)

    content = f"""---
name: {slug}
title: "{body.title}"
description: "{body.description}"
category: {body.category}
status: active
author: {body.author}
priority: {body.priority}
model_tier: "{body.model_tier}"
output_format: "{body.output_format}"
triggers:
{triggers_yaml}
tools_granted:
{tools_yaml}
depends_on:
{deps_yaml}
examples:
{examples_yaml}
---

## Instructions

{body.instructions or "Define the step-by-step instructions for this skill."}

## Rules

- Follow the user's preferences from PROFILE.md
- Check CONTRACT.md permissions before taking action
"""

    from agntspace.vault.writer import VaultWriter

    writer = VaultWriter(settings.vault_dir)
    path = f"system/skills/{body.category}/{slug}/SKILL.md"
    writer.write(path, content)
    skills.load_all()
    return {"status": "created", "name": slug, "path": path}


@router.get("/{skill_name}/files/{file_path:path}")
async def get_skill_file(request: Request, skill_name: str, file_path: str) -> dict:
    """Read an additional file from a skill folder (template, example, etc.)."""
    skills = request.app.state.skills
    content = skills.get_skill_file(skill_name, file_path)
    if content is None:
        raise HTTPException(status_code=404, detail=f"File not found: {file_path}")
    return {"path": file_path, "content": content}


@router.put("/{skill_name}/files/{file_path:path}")
async def update_skill_file(request: Request, skill_name: str, file_path: str, body: SkillFileRequest) -> dict:
    """Write an additional file to a skill folder."""
    settings = request.app.state.settings
    skills = request.app.state.skills
    skill = skills.get_skill(skill_name)
    if not skill:
        raise HTTPException(status_code=404, detail=f"Skill not found: {skill_name}")

    from pathlib import Path as P

    skill_dir = P(skill.path).parent
    full_path = f"system/skills/{skill_dir}/{file_path}"

    from agntspace.vault.writer import VaultWriter

    writer = VaultWriter(settings.vault_dir)
    writer.write(full_path, body.content)
    skills.load_all()
    return {"status": "updated", "path": full_path}
