"""MCP server exposing footage library search and browsing tools."""

import json
import logging
import os
import shutil
import sys
import threading
from pathlib import Path

from mcp.server.fastmcp import Context, FastMCP, Image

from . import DEFAULT_DB_PATH
from .db import Database
from .index import index_file, get_transcriber, IndexFileResult
from .metadata import scan_directory
from .search import search_clips
from .vision import extract_frames as _extract_frames_from_video

# All logging to stderr (critical for stdio transport)
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    stream=sys.stderr,
)
logger = logging.getLogger(__name__)

mcp = FastMCP("takeseek")

_db_instance = None

# Background transcription state
_transcription_lock = threading.Lock()
_transcription_status = {
    "running": False,
    "total": 0,
    "completed": 0,
    "errors": [],
}


def _get_db() -> Database:
    global _db_instance
    if _db_instance is None:
        db_path = os.environ.get("FOOTAGE_DB_PATH", DEFAULT_DB_PATH)
        logger.info("Opening database: %s", db_path)
        _db_instance = Database(db_path)
    return _db_instance


def _get_library_path() -> str | None:
    return os.environ.get("FOOTAGE_LIBRARY_PATH")


def _get_whisper_model() -> str:
    return os.environ.get("FOOTAGE_WHISPER_MODEL", "base")


def _get_frame_interval() -> float:
    return float(os.environ.get("FOOTAGE_FRAME_INTERVAL", "30.0"))


def _check_dependencies() -> dict:
    """Check which optional system/Python dependencies are available."""
    deps = {}
    deps["ffmpeg"] = shutil.which("ffmpeg") is not None
    deps["ffprobe"] = shutil.which("ffprobe") is not None

    try:
        import faster_whisper  # noqa: F401
        deps["faster_whisper"] = True
    except ImportError:
        deps["faster_whisper"] = False

    return deps


def _run_background_transcription(db_path: str, clip_ids: list[int], whisper_model: str):
    """Transcribe clips in a background thread using a separate DB connection."""
    global _transcription_status

    with _transcription_lock:
        _transcription_status = {
            "running": True,
            "total": len(clip_ids),
            "completed": 0,
            "errors": [],
        }

    try:
        transcriber = get_transcriber(whisper_model)
    except Exception as e:
        logger.error("Background transcription: failed to create transcriber: %s", e)
        with _transcription_lock:
            _transcription_status["running"] = False
            _transcription_status["errors"].append(f"Could not create transcriber: {e}")
        return

    # Use a separate DB connection for this thread
    db = Database(db_path)

    for clip_id in clip_ids:
        try:
            clip = db.get_clip_by_id(clip_id)
            if not clip:
                continue

            file_path = Path(clip["file_path"])
            if not file_path.exists():
                continue

            # Skip if already transcribed
            existing_transcript = db.get_transcript(clip_id)
            if existing_transcript:
                with _transcription_lock:
                    _transcription_status["completed"] += 1
                continue

            logger.info("Background transcription: %s", clip["file_name"])
            segments = transcriber.transcribe(file_path)
            seg_dicts = [{"start": s.start, "end": s.end, "text": s.text} for s in segments]
            db.insert_transcript_segments(clip_id, seg_dicts)
            db.update_clip_status(clip_id, "transcribed")

            with _transcription_lock:
                _transcription_status["completed"] += 1

            logger.info("Background transcription: %s done (%d segments)",
                        clip["file_name"], len(segments))

        except Exception as e:
            logger.error("Background transcription failed for clip %d: %s", clip_id, e)
            with _transcription_lock:
                _transcription_status["completed"] += 1
                _transcription_status["errors"].append(
                    f"clip {clip_id} ({clip.get('file_name', '?')}): {e}"
                )

    db.close()

    with _transcription_lock:
        _transcription_status["running"] = False

    logger.info("Background transcription complete: %d/%d",
                _transcription_status["completed"], _transcription_status["total"])


@mcp.tool()
def search_footage(query: str, limit: int = 20) -> str:
    """Search across video transcripts and frame descriptions.

    Args:
        query: Search terms (supports FTS5 syntax like AND, OR, NOT)
        limit: Maximum number of results to return (default 20)
    """
    try:
        db = _get_db()
        results = search_clips(db, query, limit=limit)

        # Hint about indexing if DB is empty or no results
        counts = db.get_clip_count()
        if counts.get("total", 0) == 0:
            return json.dumps({
                "results": [],
                "hint": "The database is empty. Use the index_new_files tool to scan and index a video folder first.",
            }, indent=2)

        if not results.get("transcript_matches") and not results.get("frame_matches"):
            library_path = _get_library_path()
            hint = "No results found."
            if library_path:
                hint += f" You can try indexing more files with index_new_files (library: {library_path})."
            return json.dumps({"results": results, "hint": hint}, indent=2)

        return json.dumps(results, indent=2)
    except Exception as e:
        logger.error("search_footage failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
def get_clip_details(clip_id: int) -> str:
    """Get full details for a specific clip including metadata, tags, transcript, and frame descriptions.

    Args:
        clip_id: The clip's database ID
    """
    try:
        db = _get_db()
        clip = db.get_clip_by_id(clip_id)
        if not clip:
            return json.dumps({"error": f"Clip {clip_id} not found"})

        clip["tags"] = db.get_tags(clip_id)
        clip["transcript"] = db.get_transcript(clip_id)
        clip["frame_descriptions"] = db.get_frame_descriptions(clip_id)
        return json.dumps(clip, indent=2)
    except Exception as e:
        logger.error("get_clip_details failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
def browse_folder(folder: str = None, limit: int = 50, offset: int = 0) -> str:
    """Browse clips in the library, optionally filtered by folder path.

    Args:
        folder: Filter by folder path prefix (optional)
        limit: Maximum clips to return (default 50)
        offset: Skip this many clips (for pagination)
    """
    try:
        db = _get_db()
        if folder:
            rows = db.conn.execute(
                "SELECT * FROM clips WHERE file_path LIKE ? ORDER BY file_path LIMIT ? OFFSET ?",
                (f"{folder}%", limit, offset),
            ).fetchall()
            clips = [dict(r) for r in rows]
        else:
            clips = db.list_clips(limit=limit, offset=offset)

        # Also return directory listing
        dirs = db.list_subdirectories()
        return json.dumps({"directories": dirs, "clips": clips}, indent=2)
    except Exception as e:
        logger.error("browse_folder failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
def get_transcript(clip_id: int) -> str:
    """Get the full transcript for a clip.

    Args:
        clip_id: The clip's database ID
    """
    try:
        db = _get_db()
        clip = db.get_clip_by_id(clip_id)
        if not clip:
            return json.dumps({"error": f"Clip {clip_id} not found"})

        segments = db.get_transcript(clip_id)
        return json.dumps({
            "clip_id": clip_id,
            "file_path": clip["file_path"],
            "segments": segments,
        }, indent=2)
    except Exception as e:
        logger.error("get_transcript failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
def list_tags() -> str:
    """List all tags in the library with clip counts."""
    try:
        db = _get_db()
        tags = db.list_all_tags()
        return json.dumps(tags, indent=2)
    except Exception as e:
        logger.error("list_tags failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
def add_tag(clip_id: int, tag: str) -> str:
    """Add a tag to a clip.

    Args:
        clip_id: The clip's database ID
        tag: Tag to add (will be lowercased)
    """
    try:
        db = _get_db()
        clip = db.get_clip_by_id(clip_id)
        if not clip:
            return json.dumps({"error": f"Clip {clip_id} not found"})

        db.add_tag(clip_id, tag)
        return json.dumps({"ok": True, "clip_id": clip_id, "tag": tag.lower().strip()})
    except Exception as e:
        logger.error("add_tag failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
def index_status() -> str:
    """Get indexing status summary — counts of clips by processing stage, server configuration, and dependency status.

    Also reports background transcription progress and flags incomplete clips.
    """
    try:
        db = _get_db()
        counts = db.get_clip_count()

        config = {
            "library_path": _get_library_path(),
            "whisper_model": _get_whisper_model(),
            "frame_interval": _get_frame_interval(),
            "db_path": os.environ.get("FOOTAGE_DB_PATH", DEFAULT_DB_PATH),
        }

        deps = _check_dependencies()
        missing = [name for name, ok in deps.items() if not ok]

        result = {"counts": counts, "config": config, "dependencies": deps}

        if missing:
            hints = []
            if not deps["ffmpeg"] or not deps["ffprobe"]:
                hints.append("Install ffmpeg: brew install ffmpeg (macOS) or sudo apt install ffmpeg (Linux)")
            if not deps["faster_whisper"]:
                hints.append("Install faster-whisper: pip install faster-whisper")
            result["missing_dependencies"] = missing
            result["install_hints"] = hints

        # Report background transcription progress
        with _transcription_lock:
            if _transcription_status["running"]:
                result["transcription"] = {
                    "status": "running",
                    "progress": f"{_transcription_status['completed']}/{_transcription_status['total']}",
                }
            elif _transcription_status["total"] > 0:
                result["transcription"] = {
                    "status": "complete",
                    "progress": f"{_transcription_status['completed']}/{_transcription_status['total']}",
                }
                if _transcription_status["errors"]:
                    result["transcription"]["errors"] = _transcription_status["errors"]

        # Flag incomplete clips
        actions_needed = []
        needs_transcription = counts.get("metadata", 0)
        needs_frames = counts.get("transcribed", 0) + counts.get("metadata", 0)

        if needs_transcription > 0:
            with _transcription_lock:
                if _transcription_status["running"]:
                    actions_needed.append(
                        f"{needs_transcription} clip(s) being transcribed in background "
                        f"({_transcription_status['completed']}/{_transcription_status['total']} done)."
                    )
                else:
                    actions_needed.append(
                        f"{needs_transcription} clip(s) need transcription. "
                        "Run index_new_files with reindex=True to transcribe them."
                    )

        incomplete_without_frames = counts.get("metadata", 0) + counts.get("transcribed", 0)
        if incomplete_without_frames > 0:
            actions_needed.append(
                f"{incomplete_without_frames} clip(s) have no frame descriptions. "
                "Run extract_frames on each, describe what you see, then call save_frame_descriptions."
            )

        if actions_needed:
            result["actions_needed"] = actions_needed

        return json.dumps(result, indent=2)
    except Exception as e:
        logger.error("index_status failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.tool()
async def index_new_files(ctx: Context, path: str = None, reindex: bool = False) -> str:
    """Scan a directory for video files, extract metadata, pre-extract frames, and start background transcription.

    This does three things:
    1. Extracts metadata for each video (fast)
    2. Pre-extracts frames from each video to disk (so extract_frames is instant later)
    3. Starts Whisper transcription in the background (runs while you do frame descriptions)

    After this returns, immediately call extract_frames for each clip — frames are already
    extracted and ready. Transcription will finish in the background.

    Requires ffmpeg and faster-whisper. If this returns an error about missing
    dependencies, install them (pip install faster-whisper, brew/apt install ffmpeg)
    and retry.

    Args:
        path: Directory to scan (defaults to FOOTAGE_LIBRARY_PATH env var)
        reindex: If True, re-process files even if already indexed (default False)
    """
    try:
        # Resolve directory
        scan_path = path or _get_library_path()
        if not scan_path:
            return json.dumps({
                "error": "No path provided and FOOTAGE_LIBRARY_PATH is not set. "
                         "Pass a path argument or set the FOOTAGE_LIBRARY_PATH environment variable."
            })

        scan_dir = Path(scan_path).resolve()
        if not scan_dir.is_dir():
            return json.dumps({"error": f"Directory not found: {scan_dir}"})

        db = _get_db()
        db_path = db.db_path
        whisper_model = _get_whisper_model()
        frame_interval = _get_frame_interval()

        # Check dependencies before starting
        deps = _check_dependencies()
        missing = [name for name, ok in deps.items() if not ok]
        if missing:
            install_cmds = []
            if not deps["ffmpeg"] or not deps["ffprobe"]:
                install_cmds.append("brew install ffmpeg (macOS) or sudo apt install ffmpeg (Linux)")
            if not deps["faster_whisper"]:
                install_cmds.append("pip install faster-whisper")
            return json.dumps({
                "error": "Missing required dependencies. Install them before indexing.",
                "missing": missing,
                "install_commands": install_cmds,
                "action_required": "Run the install commands above, then retry index_new_files.",
            }, indent=2)

        # Find clips whose files are missing
        missing_before = db.find_missing_files(str(scan_dir))

        # Scan for video files
        files = scan_directory(scan_dir, recursive=True)
        if not files:
            return json.dumps({"message": f"No video files found in {scan_dir}"})

        await ctx.report_progress(0, len(files))

        # Stage 1: Extract metadata for all files (fast)
        results = []
        stats = {"indexed": 0, "skipped": 0, "errors": 0}
        clip_ids_for_transcription = []

        for i, file_path in enumerate(files):
            result = index_file(
                db,
                file_path,
                do_transcribe=False,  # transcription happens in background
                skip_if_done=not reindex,
            )

            stats[result.status] = stats.get(result.status, 0) + 1
            results.append({
                "file": result.file_name,
                "status": result.status,
                "clip_id": result.clip_id,
            })

            if result.clip_id:
                clip_ids_for_transcription.append(result.clip_id)

            await ctx.report_progress(i + 1, len(files))

        # Stage 2: Pre-extract frames for all clips so extract_frames is instant later
        pre_extracted = 0
        for r in results:
            if not r["clip_id"]:
                continue
            clip = db.get_clip_by_id(r["clip_id"])
            if not clip:
                continue
            duration = clip.get("duration") or 0
            if duration <= 0:
                continue
            fp = Path(clip["file_path"])
            if not fp.exists():
                continue
            try:
                frames = _extract_frames_from_video(fp, duration, frame_interval)
                if frames:
                    # Store frame paths in a temp cache so extract_frames can use them
                    _frame_cache[r["clip_id"]] = frames
                    pre_extracted += 1
            except Exception as e:
                logger.warning("Pre-extract frames failed for %s: %s", r["file"], e)

        # Stage 3: Start background transcription
        transcription_started = False
        if clip_ids_for_transcription:
            with _transcription_lock:
                already_running = _transcription_status["running"]

            if not already_running:
                thread = threading.Thread(
                    target=_run_background_transcription,
                    args=(db_path, clip_ids_for_transcription, whisper_model),
                    daemon=True,
                )
                thread.start()
                transcription_started = True
                logger.info("Started background transcription for %d clips",
                            len(clip_ids_for_transcription))

        # Clean up missing files
        still_missing = [m for m in missing_before if not db.get_clip_by_id(m["clip_id"]) or
                         db.get_clip_by_id(m["clip_id"])["file_path"] == m["file_path"]]
        removed = []
        for m in still_missing:
            clip = db.get_clip_by_id(m["clip_id"])
            if clip and not Path(clip["file_path"]).exists():
                db.delete_clip(m["clip_id"])
                removed.append(m)

        # Build summary
        clips_for_frames = [r for r in results if r["clip_id"]]

        summary = {
            "directory": str(scan_dir),
            "total_files": len(files),
            "stats": stats,
            "files": results,
            "frames_pre_extracted": pre_extracted,
        }

        if transcription_started:
            summary["transcription"] = (
                f"Started in background for {len(clip_ids_for_transcription)} clip(s). "
                "Transcription will complete while you work on frame descriptions. "
                "Check progress with index_status."
            )

        if removed:
            summary["removed_missing"] = len(removed)

        if clips_for_frames:
            summary["next_step"] = (
                "IMPORTANT: Do not stop here. Transcription is running in the background. "
                "Immediately proceed to call extract_frames for each clip listed below, "
                "then describe what you see in each frame, then call save_frame_descriptions "
                "with your descriptions. Do not ask the user for confirmation — this is part "
                "of the indexing process."
            )
            summary["clips_needing_frames"] = [
                {"clip_id": r["clip_id"], "file": r["file"]}
                for r in clips_for_frames
            ]

        return json.dumps(summary, indent=2)

    except Exception as e:
        logger.error("index_new_files failed: %s", e)
        return json.dumps({"error": str(e)})


# Frame cache: clip_id -> list of (timestamp, Path) from pre-extraction
_frame_cache: dict[int, list[tuple[float, Path]]] = {}


@mcp.tool()
def extract_frames(clip_id: int, frame_interval: float = None) -> list:
    """Extract frames from a video clip and return them as images for you to see.

    If index_new_files pre-extracted frames, this returns them instantly from cache.
    Otherwise extracts on demand.

    Returns a list of images with timestamp labels. Describe what you see in each
    frame, then call save_frame_descriptions to store your descriptions.

    Args:
        clip_id: The clip's database ID
        frame_interval: Seconds between frames (default: FOOTAGE_FRAME_INTERVAL or 30s)
    """
    try:
        db = _get_db()
        clip = db.get_clip_by_id(clip_id)
        if not clip:
            return [f"Error: Clip {clip_id} not found"]

        file_path = Path(clip["file_path"])
        if not file_path.exists():
            return [f"Error: File not found: {file_path}"]

        duration = clip.get("duration") or 0
        if duration <= 0:
            return [f"Error: Clip {clip_id} has no duration info — cannot extract frames"]

        interval = frame_interval or _get_frame_interval()

        # Use pre-extracted frames if available
        if clip_id in _frame_cache:
            frames = _frame_cache.pop(clip_id)
            # Verify frames still exist on disk
            frames = [(ts, p) for ts, p in frames if p.exists()]
        else:
            frames = _extract_frames_from_video(file_path, duration, interval)

        if not frames:
            return ["No frames could be extracted from this video."]

        # Build mixed content: text label + Image for each frame
        result = []
        result.append(
            f"Extracted {len(frames)} frame(s) from '{clip['file_name']}' "
            f"(clip_id={clip_id}, duration={duration:.1f}s, interval={interval:.1f}s). "
            "Please describe what you see in each frame, then call "
            "save_frame_descriptions with your descriptions."
        )
        for ts, frame_path in frames:
            result.append(f"Frame at {ts:.1f}s:")
            result.append(Image(path=frame_path))

        return result

    except Exception as e:
        logger.error("extract_frames failed: %s", e)
        return [f"Error: {e}"]


@mcp.tool()
def save_frame_descriptions(clip_id: int, descriptions: str) -> str:
    """Save frame descriptions for a clip after viewing its frames.

    Call this after extract_frames with your descriptions of each frame.

    Args:
        clip_id: The clip's database ID
        descriptions: JSON array of objects, each with "timestamp" (float) and "description" (string).
                      Example: [{"timestamp": 0.0, "description": "A wide shot of a forest..."}, ...]
    """
    try:
        db = _get_db()
        clip = db.get_clip_by_id(clip_id)
        if not clip:
            return json.dumps({"error": f"Clip {clip_id} not found"})

        try:
            desc_list = json.loads(descriptions)
        except json.JSONDecodeError as e:
            return json.dumps({"error": f"Invalid JSON: {e}"})

        if not isinstance(desc_list, list):
            return json.dumps({"error": "descriptions must be a JSON array"})

        for item in desc_list:
            if not isinstance(item, dict) or "timestamp" not in item or "description" not in item:
                return json.dumps({
                    "error": "Each item must have 'timestamp' (number) and 'description' (string)"
                })

        db.insert_frame_descriptions(clip_id, desc_list)

        # Update clip status to reflect vision is done
        current_status = clip.get("index_status", "pending")
        if current_status in ("metadata", "transcribed"):
            db.update_clip_status(clip_id, "complete")
        elif current_status == "vision_done":
            db.update_clip_status(clip_id, "complete")

        return json.dumps({
            "ok": True,
            "clip_id": clip_id,
            "saved": len(desc_list),
            "index_status": "complete",
        })

    except Exception as e:
        logger.error("save_frame_descriptions failed: %s", e)
        return json.dumps({"error": str(e)})


@mcp.prompt()
def index_footage(path: str = None) -> str:
    """Fully index video footage: extract metadata, transcribe audio, extract frames, and describe visual content.

    This orchestrates the complete indexing pipeline. It will:
    1. Run index_new_files to scan for videos (metadata + background transcription + frame pre-extraction)
    2. For each clip: extract_frames to view the pre-extracted frames
    3. Describe what you see in each frame
    4. Save descriptions with save_frame_descriptions

    Args:
        path: Directory to scan (optional, defaults to configured library path)
    """
    path_instruction = f' with path "{path}"' if path else ""

    return f"""Fully index the video footage library. Do NOT ask for confirmation between steps — complete the entire pipeline automatically.

Step 1: Call `index_status` to check dependencies. If any are missing, install them (e.g. run `pip install faster-whisper` or `apt install ffmpeg` / `brew install ffmpeg`) before proceeding.

Step 2: Call `index_new_files`{path_instruction} to scan for videos and extract metadata. This will automatically start background transcription and pre-extract frames. Do not wait for transcription to finish.

Step 3: Immediately after step 2 completes, for EVERY clip in the results, call `extract_frames` with that clip's `clip_id`. Frames are pre-extracted so this will be fast. Do not skip this step. Do not ask the user if they want to proceed.

Step 4: After viewing the extracted frames from each clip, write detailed descriptions of what you see in each frame (scene, subjects, actions, setting, lighting, any visible text).

Step 5: Call `save_frame_descriptions` for each clip with your descriptions as a JSON array of objects, each with "timestamp" (float) and "description" (string).

Step 6: After ALL clips are fully processed, report a brief summary. Transcription runs in the background and will be done by now.

IMPORTANT: This is a single automated pipeline. Do not stop to ask the user questions between steps. If a dependency is missing, install it and continue. Process every clip end-to-end."""


def main():
    logger.info("Starting TakeSeek MCP server (stdio)")
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()
