from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any

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

from prompt_auto_lab.api.models import PromptFile, PromptValidation
from prompt_auto_lab.api.services import picasso_service, user_service
from prompt_auto_lab.core.store_dir import get_store_dir

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


class PromptUpdateRequest(BaseModel):
    content: str


class PromptValidateRequest(BaseModel):
    files: dict[str, str] | None = None  # filename -> content (optional for batch)
    content: str | None = None  # single content string (for single file validation)


def _get_picasso_path(user_id: str | None = None, session_id: str | None = None) -> Path:
    """
    Get Picasso repository path.
    Requires user_id and session_id to locate the session-specific copy.
    """
    if user_id and session_id:
        return picasso_service.get_session_picasso_path(user_id, session_id)
    raise HTTPException(
        status_code=400,
        detail="user_id and session_id are required to resolve Picasso path",
    )


def _load_session_config(user_id: str, session_id: str) -> dict[str, Any]:
    """Load session config from disk."""
    store_dir = get_store_dir()
    config_file = store_dir / "users" / user_id / "sessions" / session_id / "session_config.json"
    if not config_file.exists():
        return {}
    try:
        return json.loads(config_file.read_text(encoding="utf-8"))
    except Exception:
        return {}


# Default prompt files as fallback when no session config is available
_DEFAULT_PROMPT_PATHS = {
    "clarifying": Path("Service.Chat") / "Inference" / "Prompts" / "Templates" / "responding-common" / "clarifying.liquid",
    "idiolect": Path("Service.Chat") / "Inference" / "Prompts" / "Templates" / "responding-common" / "blocks" / "persona" / "idiolect.liquid",
    "detailed_format": Path("Service.Chat") / "Inference" / "Prompts" / "Templates" / "responding-common" / "format" / "detailed_format.liquid",
}


def _get_prompt_files(user_id: str | None = None, session_id: str | None = None) -> dict[str, Path]:
    """
    Get prompt file paths.

    If a session with liquid_prompt_entries is available, derives files from those entries.
    Otherwise falls back to hardcoded defaults.
    """
    repo_path = _get_picasso_path(user_id, session_id)

    # Try to load session config and use liquid_prompt_entries
    if user_id and session_id:
        config = _load_session_config(user_id, session_id)
        entries = config.get("liquid_prompt_entries")
        if entries:
            files: dict[str, Path] = {}
            for entry in entries:
                path_str = entry.get("path", "")
                if not path_str:
                    continue
                # Use the file stem (without .liquid) as the key
                entry_path = Path(path_str)
                name = entry_path.stem
                # Ensure uniqueness by appending parent dir if needed
                if name in files:
                    parent = entry_path.parent.name
                    name = f"{parent}_{name}"
                files[name] = repo_path / path_str
            if files:
                return files

    # Fallback to defaults
    return {name: repo_path / rel_path for name, rel_path in _DEFAULT_PROMPT_PATHS.items()}


@router.get("/files")
async def list_prompt_files(
    session_id: str | None = None,
    x_user_id: str = Header(None),
) -> list[dict[str, str | bool]]:
    """List available prompt files."""
    # Use session-specific Picasso if user and session provided
    prompt_files = _get_prompt_files(x_user_id, session_id)
    result = []
    for name, path in prompt_files.items():
        result.append({
            "name": name,
            "path": str(path),
            "exists": path.exists(),
        })
    return result


@router.get("/files/{name}")
async def get_prompt_file(
    name: str,
    session_id: str | None = None,
    x_user_id: str = Header(None),
) -> PromptFile:
    """Get prompt file content."""
    prompt_files = _get_prompt_files(x_user_id, session_id)

    # Handle .liquid suffix
    normalized_name = name
    if name.endswith(".liquid"):
        normalized_name = name[:-7]

    if normalized_name not in prompt_files:
        raise HTTPException(
            status_code=404,
            detail=f"Unknown file: {name}. Valid names: {list(prompt_files.keys())}"
        )

    file_path = prompt_files[normalized_name]
    if not file_path.exists():
        raise HTTPException(
            status_code=404,
            detail=f"File not found: {file_path}"
        )

    content = file_path.read_text(encoding="utf-8")
    return PromptFile(
        name=normalized_name,
        path=str(file_path),
        content=content,
        modified=False,
    )


@router.get("/files/{name}/at/{commit_id}")
async def get_prompt_file_at_commit(
    name: str,
    commit_id: str,
    session_id: str | None = None,
    x_user_id: str = Header(None),
) -> PromptFile:
    """Get prompt file content at a specific git commit."""
    if not x_user_id:
        raise HTTPException(status_code=401, detail="X-User-Id header required")
    if not session_id:
        raise HTTPException(status_code=400, detail="session_id is required")

    prompt_files = _get_prompt_files(x_user_id, session_id)

    # Handle .liquid suffix
    normalized_name = name
    if name.endswith(".liquid"):
        normalized_name = name[:-7]

    if normalized_name not in prompt_files:
        raise HTTPException(
            status_code=404,
            detail=f"Unknown file: {name}. Valid names: {list(prompt_files.keys())}"
        )

    file_path = prompt_files[normalized_name]
    repo_path = _get_picasso_path(x_user_id, session_id)

    # Calculate relative path from repo root
    try:
        relative_path = file_path.relative_to(repo_path)
    except ValueError:
        raise HTTPException(
            status_code=500,
            detail=f"File path {file_path} is not under repo {repo_path}"
        )

    # Run git show to get content at commit
    import subprocess
    try:
        result = subprocess.run(
            ["git", "show", f"{commit_id}:{relative_path.as_posix()}"],
            cwd=str(repo_path),
            capture_output=True,
            text=True,
            encoding="utf-8",
            timeout=10,
        )
        if result.returncode != 0:
            # Check if it's a "file not found at commit" error
            if "does not exist" in result.stderr or "fatal: path" in result.stderr:
                raise HTTPException(
                    status_code=404,
                    detail=f"File {normalized_name} did not exist at commit {commit_id[:7]}"
                )
            raise HTTPException(
                status_code=400,
                detail=f"Git error: {result.stderr.strip()}"
            )
        content = result.stdout
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=504, detail="Git command timed out")
    except FileNotFoundError:
        raise HTTPException(status_code=500, detail="Git not found on system")

    return PromptFile(
        name=normalized_name,
        path=str(file_path),
        content=content,
        modified=False,
    )


@router.get("/diff/{base_commit}/{target_commit}")
async def get_diff_between_commits(
    base_commit: str,
    target_commit: str,
    session_id: str,
    x_user_id: str = Header(None),
) -> dict[str, Any]:
    """Get diff of all changed files between two commits."""
    if not x_user_id:
        raise HTTPException(status_code=401, detail="X-User-Id header required")
    if not session_id:
        raise HTTPException(status_code=400, detail="session_id is required")

    repo_path = _get_picasso_path(x_user_id, session_id)

    import subprocess

    # Truncate commit hashes to 8 chars to avoid Windows MAX_PATH issues
    # The full path "repo_path/commit:file_path" can exceed 260 chars on Windows
    # when using full 40-char commit hashes with long file paths
    base_commit_short = base_commit[:8] if len(base_commit) > 8 else base_commit
    target_commit_short = target_commit[:8] if len(target_commit) > 8 else target_commit

    # Get list of changed files
    try:
        result = subprocess.run(
            ["git", "diff", "--name-only", base_commit_short, target_commit_short],
            cwd=str(repo_path),
            capture_output=True,
            text=True,
            encoding="utf-8",
            timeout=10,
        )
        if result.returncode != 0:
            raise HTTPException(
                status_code=400,
                detail=f"Git error: {result.stderr.strip()}"
            )
        changed_files = [f.strip() for f in result.stdout.strip().split('\n') if f.strip()]
    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=504, detail="Git command timed out")
    except FileNotFoundError:
        raise HTTPException(status_code=500, detail="Git not found on system")

    # Get content for each changed file at both commits
    files = []
    for file_path in changed_files:
        # Get base content
        base_content = ""
        try:
            base_result = subprocess.run(
                ["git", "show", f"{base_commit_short}:{file_path}"],
                cwd=str(repo_path),
                capture_output=True,
                text=True,
                encoding="utf-8",
                timeout=10,
            )
            if base_result.returncode == 0:
                base_content = base_result.stdout
        except Exception:
            pass

        # Get target content
        target_content = ""
        try:
            target_result = subprocess.run(
                ["git", "show", f"{target_commit_short}:{file_path}"],
                cwd=str(repo_path),
                capture_output=True,
                text=True,
                encoding="utf-8",
                timeout=10,
            )
            if target_result.returncode == 0:
                target_content = target_result.stdout
        except Exception:
            pass

        # Extract short name (last part of path)
        short_name = file_path.split('/')[-1]

        files.append({
            "path": file_path,
            "short_name": short_name,
            "base_content": base_content,
            "target_content": target_content,
        })

    return {
        "files": files,
        "base_commit": base_commit,
        "target_commit": target_commit,
    }


@router.put("/files/{name}")
async def update_prompt_file(
    name: str,
    request: PromptUpdateRequest,
    session_id: str | None = None,
    x_user_id: str = Header(None),
) -> PromptFile:
    """Update prompt file content."""
    if not x_user_id:
        raise HTTPException(status_code=401, detail="X-User-Id header required")
    if not session_id:
        raise HTTPException(status_code=400, detail="session_id is required for updates")
    prompt_files = _get_prompt_files(x_user_id, session_id)

    # Handle .liquid suffix
    normalized_name = name
    if name.endswith(".liquid"):
        normalized_name = name[:-7]

    if normalized_name not in prompt_files:
        raise HTTPException(
            status_code=404,
            detail=f"Unknown file: {name}. Valid names: {list(prompt_files.keys())}"
        )

    file_path = prompt_files[normalized_name]

    # Write new content
    file_path.write_text(request.content, encoding="utf-8")

    return PromptFile(
        name=normalized_name,
        path=str(file_path),
        content=request.content,
        modified=True,
    )


@router.post("/validate")
async def validate_prompts(request: PromptValidateRequest) -> dict[str, Any]:
    """Validate Liquid syntax for multiple files or a single content string."""
    # Support single content string for simple validation
    if request.content is not None:
        errors = _validate_liquid_content(request.content)
        return {"valid": len(errors) == 0, "error": errors[0] if errors else None}

    # Support batch validation with files dict
    if request.files is None:
        return {"valid": False, "error": "Either 'content' or 'files' must be provided"}

    results = {}
    for name, content in request.files.items():
        errors = _validate_liquid_content(content)
        results[name] = PromptValidation(ok=len(errors) == 0, errors=errors)
    return results


def _validate_liquid_content(content: str) -> list[str]:
    """Validate Liquid template content and return list of errors."""
    errors = []

    # 1. Check basic Liquid tag pairing {% %}
    open_tags = content.count("{%")
    close_tags = content.count("%}")
    if open_tags != close_tags:
        errors.append(f"Mismatched Liquid tags: {open_tags} '{{%' vs {close_tags} '%}}'")

    # 2. Check variable tag pairing {{ }}
    open_vars = content.count("{{")
    close_vars = content.count("}}")
    if open_vars != close_vars:
        errors.append(f"Mismatched variable tags: {open_vars} '{{{{' vs {close_vars} '}}}}'")

    # 3. Check if common Liquid blocks are paired
    blocks = ["if", "for", "unless", "case", "capture", "comment", "raw"]
    for block in blocks:
        opens = len(re.findall(rf'\{{% *{block}\b', content))
        closes = len(re.findall(rf'\{{% *end{block} *%\}}', content))
        if opens != closes:
            errors.append(f"Mismatched {{% {block} %}}/{{% end{block} %}}: {opens} opens vs {closes} closes")

    # 4. Check if elsif/else are within if block
    elsif_count = len(re.findall(r'\{% *elsif\b', content))
    else_count = len(re.findall(r'\{% *else *%\}', content))
    if_count = len(re.findall(r'\{% *if\b', content))
    if elsif_count > 0 or else_count > 0:
        if if_count == 0:
            errors.append("Found elsif/else without any if block")

    return errors


@router.get("/diff/{v1}/{v2}")
async def get_prompt_diff(v1: int, v2: int, file: str = "clarifying") -> dict[str, Any]:
    """Get diff between two prompt versions."""
    # This would need to integrate with version storage
    # For now, return a placeholder
    return {
        "file": file,
        "version1": v1,
        "version2": v2,
        "diff": "Diff functionality requires version storage integration",
    }
