"""MCP tools for file tracking, associations, and finding triage."""

from __future__ import annotations

import logging
import sqlite3
from collections.abc import Callable
from typing import Any

from mcp.types import TextContent, Tool

from filigree.core import VALID_ASSOC_TYPES, VALID_FINDING_STATUSES, VALID_SEVERITIES
from filigree.mcp_tools.common import _parse_args, _text, _validate_actor, _validate_int_range, _validate_str
from filigree.types.api import ErrorResponse
from filigree.types.inputs import (
    AddFileAssociationArgs,
    BatchUpdateFindingsArgs,
    DismissFindingArgs,
    GetFileArgs,
    GetFileTimelineArgs,
    GetFindingArgs,
    GetIssueFilesArgs,
    ListFilesArgs,
    ListFindingsArgs,
    PromoteFindingArgs,
    RegisterFileArgs,
    UpdateFindingArgs,
)

_logger = logging.getLogger(__name__)


def register() -> tuple[list[Tool], dict[str, Callable[..., Any]]]:
    """Return (tool_definitions, handler_map) for file-domain tools."""
    tools = [
        Tool(
            name="list_files",
            description="List tracked files with filtering, sorting, and pagination.",
            inputSchema={
                "type": "object",
                "properties": {
                    "limit": {"type": "integer", "default": 100, "minimum": 1, "maximum": 10000},
                    "offset": {"type": "integer", "default": 0, "minimum": 0},
                    "language": {"type": "string", "description": "Filter by language"},
                    "path_prefix": {"type": "string", "description": "Filter by substring in file path"},
                    "min_findings": {"type": "integer", "minimum": 0, "description": "Minimum open findings count"},
                    "has_severity": {
                        "type": "string",
                        "enum": sorted(VALID_SEVERITIES),
                        "description": "Require at least one open finding at this severity",
                    },
                    "scan_source": {"type": "string", "description": "Filter files by finding source"},
                    "sort": {
                        "type": "string",
                        "enum": ["updated_at", "first_seen", "path", "language"],
                        "default": "updated_at",
                    },
                    "direction": {"type": "string", "enum": ["asc", "desc"]},
                },
            },
        ),
        Tool(
            name="get_file",
            description="Get file details, linked issues, recent findings, and summary by file ID.",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_id": {"type": "string", "description": "File ID"},
                },
                "required": ["file_id"],
            },
        ),
        Tool(
            name="get_file_timeline",
            description="Get merged timeline events for a file (finding, association, metadata updates).",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_id": {"type": "string", "description": "File ID"},
                    "limit": {"type": "integer", "default": 50, "minimum": 1, "maximum": 10000},
                    "offset": {"type": "integer", "default": 0, "minimum": 0},
                    "event_type": {
                        "type": "string",
                        "enum": ["finding", "association", "file_metadata_update"],
                        "description": "Optional event type filter",
                    },
                },
                "required": ["file_id"],
            },
        ),
        Tool(
            name="get_issue_files",
            description="List files associated with an issue.",
            inputSchema={
                "type": "object",
                "properties": {
                    "issue_id": {"type": "string", "description": "Issue ID"},
                },
                "required": ["issue_id"],
            },
        ),
        Tool(
            name="add_file_association",
            description="Create a file<->issue association. Idempotent for duplicate tuples.",
            inputSchema={
                "type": "object",
                "properties": {
                    "file_id": {"type": "string", "description": "File ID"},
                    "issue_id": {"type": "string", "description": "Issue ID"},
                    "assoc_type": {
                        "type": "string",
                        "enum": sorted(VALID_ASSOC_TYPES),
                        "description": "Association type",
                    },
                },
                "required": ["file_id", "issue_id", "assoc_type"],
            },
        ),
        Tool(
            name="register_file",
            description="Register or fetch a file record by project-relative path without running a scanner.",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "File path (relative to project root)"},
                    "language": {"type": "string", "description": "Optional language hint"},
                    "file_type": {"type": "string", "description": "Optional file type tag"},
                    "metadata": {"type": "object", "description": "Optional metadata map"},
                },
                "required": ["path"],
            },
        ),
        Tool(
            name="get_finding",
            description="Get a single scan finding by ID.",
            inputSchema={
                "type": "object",
                "properties": {
                    "finding_id": {"type": "string", "description": "Finding ID"},
                },
                "required": ["finding_id"],
            },
        ),
        Tool(
            name="list_findings",
            description="List scan findings across all files with optional filters.",
            inputSchema={
                "type": "object",
                "properties": {
                    "severity": {"type": "string", "enum": sorted(VALID_SEVERITIES), "description": "Filter by severity"},
                    "status": {"type": "string", "enum": sorted(VALID_FINDING_STATUSES), "description": "Filter by finding status"},
                    "scan_source": {"type": "string", "description": "Filter by scan source"},
                    "scan_run_id": {"type": "string", "description": "Filter by scan run ID"},
                    "file_id": {"type": "string", "description": "Filter by file ID"},
                    "issue_id": {"type": "string", "description": "Filter by linked issue ID"},
                    "limit": {"type": "integer", "default": 100, "minimum": 1, "maximum": 10000},
                    "offset": {"type": "integer", "default": 0, "minimum": 0},
                },
            },
        ),
        Tool(
            name="update_finding",
            description="Update a finding's status or linked issue.",
            inputSchema={
                "type": "object",
                "properties": {
                    "finding_id": {"type": "string", "description": "Finding ID"},
                    "status": {"type": "string", "enum": sorted(VALID_FINDING_STATUSES), "description": "New finding status"},
                    "issue_id": {"type": "string", "description": "Issue ID to link"},
                },
                "required": ["finding_id"],
            },
        ),
        Tool(
            name="batch_update_findings",
            description="Update the status of multiple findings at once.",
            inputSchema={
                "type": "object",
                "properties": {
                    "finding_ids": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "List of finding IDs to update",
                    },
                    "status": {"type": "string", "enum": sorted(VALID_FINDING_STATUSES), "description": "New status for all findings"},
                },
                "required": ["finding_ids", "status"],
            },
        ),
        Tool(
            name="promote_finding",
            description="Promote a scan finding to an observation for triage tracking.",
            inputSchema={
                "type": "object",
                "properties": {
                    "finding_id": {"type": "string", "description": "Finding ID"},
                    "priority": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 4,
                        "description": "Override priority (default: inferred from severity)",
                    },
                    "actor": {"type": "string", "description": "Actor identity"},
                },
                "required": ["finding_id"],
            },
        ),
        Tool(
            name="dismiss_finding",
            description="Dismiss a finding by marking it as false_positive.",
            inputSchema={
                "type": "object",
                "properties": {
                    "finding_id": {"type": "string", "description": "Finding ID"},
                    "reason": {"type": "string", "description": "Optional reason for dismissal"},
                },
                "required": ["finding_id"],
            },
        ),
    ]

    handlers: dict[str, Callable[..., Any]] = {
        "list_files": _handle_list_files,
        "get_file": _handle_get_file,
        "get_file_timeline": _handle_get_file_timeline,
        "get_issue_files": _handle_get_issue_files,
        "add_file_association": _handle_add_file_association,
        "register_file": _handle_register_file,
        "get_finding": _handle_get_finding,
        "list_findings": _handle_list_findings,
        "update_finding": _handle_update_finding,
        "batch_update_findings": _handle_batch_update_findings,
        "promote_finding": _handle_promote_finding,
        "dismiss_finding": _handle_dismiss_finding,
    }

    return tools, handlers


# ---------------------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------------------


async def _handle_list_files(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, ListFilesArgs)
    tracker = _get_db()
    limit = args.get("limit", 100)
    offset = args.get("offset", 0)
    min_findings = args.get("min_findings")
    has_severity = args.get("has_severity")
    language = args.get("language")
    path_prefix = args.get("path_prefix")
    scan_source = args.get("scan_source")
    sort = args.get("sort", "updated_at")
    direction = args.get("direction")
    valid_sorts = {"updated_at", "first_seen", "path", "language"}

    for err in (
        _validate_int_range(limit, "limit", min_val=1, max_val=10000),
        _validate_int_range(offset, "offset", min_val=0),
        _validate_int_range(min_findings, "min_findings", min_val=0),
        _validate_str(language, "language"),
        _validate_str(path_prefix, "path_prefix"),
        _validate_str(scan_source, "scan_source"),
    ):
        if err is not None:
            return err
    if has_severity is not None and (not isinstance(has_severity, str) or has_severity not in VALID_SEVERITIES):
        return _text(ErrorResponse(error=f"has_severity must be one of {sorted(VALID_SEVERITIES)}", code="validation_error"))
    if not isinstance(sort, str) or sort not in valid_sorts:
        return _text(ErrorResponse(error=f"sort must be one of {sorted(valid_sorts)}", code="validation_error"))
    if direction is not None and (not isinstance(direction, str) or direction.upper() not in {"ASC", "DESC"}):
        return _text(ErrorResponse(error="direction must be 'asc' or 'desc'", code="validation_error"))

    files_result = tracker.list_files_paginated(
        limit=limit,
        offset=offset,
        language=language,
        path_prefix=path_prefix,
        min_findings=min_findings,
        has_severity=has_severity,
        scan_source=scan_source,
        sort=sort,
        direction=direction,
    )
    return _text(files_result)


async def _handle_get_file(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, GetFileArgs)
    tracker = _get_db()
    file_id = args.get("file_id", "")
    if not isinstance(file_id, str) or not file_id.strip():
        return _text(ErrorResponse(error="file_id is required", code="validation_error"))
    try:
        data = tracker.get_file_detail(file_id)
    except KeyError:
        return _text(ErrorResponse(error=f"File not found: {file_id}", code="not_found"))
    return _text(data)


async def _handle_get_file_timeline(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, GetFileTimelineArgs)
    tracker = _get_db()
    file_id = args.get("file_id", "")
    limit = args.get("limit", 50)
    offset = args.get("offset", 0)
    event_type = args.get("event_type")
    valid_event_types = {"finding", "association", "file_metadata_update"}

    if not isinstance(file_id, str) or not file_id.strip():
        return _text(ErrorResponse(error="file_id is required", code="validation_error"))
    if not isinstance(limit, int) or limit < 1 or limit > 10000:
        return _text(ErrorResponse(error="limit must be an integer in [1, 10000]", code="validation_error"))
    if not isinstance(offset, int) or offset < 0:
        return _text(ErrorResponse(error="offset must be a non-negative integer", code="validation_error"))
    if event_type is not None and (not isinstance(event_type, str) or event_type not in valid_event_types):
        return _text(
            ErrorResponse(
                error=f"event_type must be one of {sorted(valid_event_types)}",
                code="validation_error",
            )
        )

    try:
        timeline_result = tracker.get_file_timeline(file_id, limit=limit, offset=offset, event_type=event_type)
    except KeyError:
        return _text(ErrorResponse(error=f"File not found: {file_id}", code="not_found"))
    return _text(timeline_result)


async def _handle_get_issue_files(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, GetIssueFilesArgs)
    tracker = _get_db()
    issue_id = args.get("issue_id", "")
    if not isinstance(issue_id, str) or not issue_id.strip():
        return _text(ErrorResponse(error="issue_id is required", code="validation_error"))
    try:
        tracker.get_issue(issue_id)
    except KeyError:
        return _text(ErrorResponse(error=f"Issue not found: {issue_id}", code="not_found"))
    return _text(tracker.get_issue_files(issue_id))


async def _handle_add_file_association(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, AddFileAssociationArgs)
    tracker = _get_db()
    file_id = args.get("file_id", "")
    issue_id = args.get("issue_id", "")
    assoc_type = args.get("assoc_type", "")

    if not isinstance(file_id, str) or not file_id.strip():
        return _text(ErrorResponse(error="file_id is required", code="validation_error"))
    if not isinstance(issue_id, str) or not issue_id.strip():
        return _text(ErrorResponse(error="issue_id is required", code="validation_error"))
    if not isinstance(assoc_type, str) or not assoc_type.strip():
        return _text(ErrorResponse(error="assoc_type is required", code="validation_error"))

    try:
        tracker.get_file(file_id)
    except KeyError:
        return _text(ErrorResponse(error=f"File not found: {file_id}", code="not_found"))

    try:
        tracker.get_issue(issue_id)
    except KeyError:
        return _text(ErrorResponse(error=f"Issue not found: {issue_id}", code="not_found"))

    try:
        tracker.add_file_association(file_id, issue_id, assoc_type)
    except ValueError as e:
        return _text(ErrorResponse(error=str(e), code="validation_error"))
    return _text({"status": "created"})


async def _handle_register_file(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db, _get_filigree_dir, _safe_path

    args = _parse_args(arguments, RegisterFileArgs)
    tracker = _get_db()
    raw_path = args.get("path", "")
    language = args.get("language", "")
    file_type = args.get("file_type", "")
    metadata = args.get("metadata")

    if not isinstance(raw_path, str) or not raw_path.strip():
        return _text(ErrorResponse(error="path is required", code="validation_error"))
    if language is not None and not isinstance(language, str):
        return _text(ErrorResponse(error="language must be a string", code="validation_error"))
    if file_type is not None and not isinstance(file_type, str):
        return _text(ErrorResponse(error="file_type must be a string", code="validation_error"))
    if metadata is not None and not isinstance(metadata, dict):
        return _text(ErrorResponse(error="metadata must be an object", code="validation_error"))

    try:
        target = _safe_path(raw_path)
    except ValueError as e:
        return _text(ErrorResponse(error=str(e), code="invalid_path"))

    filigree_dir = _get_filigree_dir()
    if filigree_dir is None:
        return _text(ErrorResponse(error="Project directory not initialized", code="not_initialized"))

    canonical_path = str(target.relative_to(filigree_dir.resolve().parent))
    file_record = tracker.register_file(
        canonical_path,
        language=language or "",
        file_type=file_type or "",
        metadata=metadata,
    )
    return _text(file_record.to_dict())


# ---------------------------------------------------------------------------
# Finding triage handlers
# ---------------------------------------------------------------------------


async def _handle_get_finding(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, GetFindingArgs)
    finding_id = args.get("finding_id", "")
    if not isinstance(finding_id, str) or not finding_id.strip():
        return _text(ErrorResponse(error="finding_id is required", code="validation_error"))
    tracker = _get_db()
    try:
        finding = tracker.get_finding(finding_id)
    except KeyError:
        return _text(ErrorResponse(error=f"Finding not found: {finding_id}", code="not_found"))
    return _text(finding)


async def _handle_list_findings(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, ListFindingsArgs)
    tracker = _get_db()
    limit = args.get("limit", 100)
    offset = args.get("offset", 0)

    for err in (
        _validate_int_range(limit, "limit", min_val=1, max_val=10000),
        _validate_int_range(offset, "offset", min_val=0),
    ):
        if err is not None:
            return err

    filters: dict[str, Any] = {}
    for key in ("severity", "status", "scan_source", "scan_run_id", "file_id", "issue_id"):
        val = args.get(key)
        if val is not None:
            filters[key] = val

    # Validate string-type filters from MCP input
    for key in ("scan_source", "scan_run_id", "file_id", "issue_id"):
        val = filters.get(key)
        if val is not None and not isinstance(val, str):
            return _text(ErrorResponse(error=f"{key} must be a string", code="validation_error"))

    try:
        result = tracker.list_findings_global(limit=limit, offset=offset, **filters)
    except ValueError as e:
        return _text(ErrorResponse(error=str(e), code="validation_error"))
    return _text(result)


async def _handle_update_finding(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, UpdateFindingArgs)
    finding_id = args.get("finding_id", "")
    if not isinstance(finding_id, str) or not finding_id.strip():
        return _text(ErrorResponse(error="finding_id is required", code="validation_error"))
    status = args.get("status")
    issue_id = args.get("issue_id")
    if status is None and issue_id is None:
        return _text(ErrorResponse(error="At least one of status or issue_id must be provided", code="validation_error"))

    tracker = _get_db()
    try:
        updated = tracker.update_finding(finding_id, status=status, issue_id=issue_id)
    except KeyError:
        return _text(ErrorResponse(error=f"Finding not found: {finding_id}", code="not_found"))
    except ValueError as e:
        return _text(ErrorResponse(error=str(e), code="validation_error"))
    return _text(updated)


async def _handle_batch_update_findings(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, BatchUpdateFindingsArgs)
    finding_ids = args.get("finding_ids", [])
    status = args.get("status", "")
    if not isinstance(finding_ids, list) or not finding_ids:
        return _text(ErrorResponse(error="finding_ids must be a non-empty list", code="validation_error"))
    if not isinstance(status, str) or not status.strip():
        return _text(ErrorResponse(error="status is required", code="validation_error"))
    if status not in VALID_FINDING_STATUSES:
        return _text(
            ErrorResponse(
                error=f"Invalid finding status: {status!r}. Valid: {', '.join(sorted(VALID_FINDING_STATUSES))}",
                code="validation_error",
            )
        )

    tracker = _get_db()
    updated: list[str] = []
    errors: list[dict[str, str]] = []
    for fid in finding_ids:
        try:
            tracker.update_finding(fid, status=status)
            updated.append(fid)
        except (KeyError, ValueError) as e:
            _logger.warning("batch_update_findings: failed for %s: %s", fid, e)
            errors.append({"finding_id": fid, "error": str(e)})
    if not updated and errors:
        return _text(
            ErrorResponse(
                error=f"All {len(errors)} finding update(s) failed",
                code="batch_all_failed",
            )
        )
    result: dict[str, Any] = {"updated": updated, "errors": errors}
    if updated and errors:
        result["partial"] = True
    return _text(result)


async def _handle_promote_finding(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, PromoteFindingArgs)
    finding_id = args.get("finding_id", "")
    if not isinstance(finding_id, str) or not finding_id.strip():
        return _text(ErrorResponse(error="finding_id is required", code="validation_error"))
    priority = args.get("priority")
    actor, actor_err = _validate_actor(args.get("actor", "mcp"))
    if actor_err:
        return actor_err

    tracker = _get_db()
    try:
        obs = tracker.promote_finding_to_observation(finding_id, priority=priority, actor=actor)
    except KeyError:
        return _text(ErrorResponse(error=f"Finding not found: {finding_id}", code="not_found"))
    except (ValueError, sqlite3.Error) as exc:
        _logger.warning("Failed to promote finding %s: %s", finding_id, exc)
        return _text(ErrorResponse(error=f"Failed to promote finding: {exc}", code="promotion_error"))
    return _text(obs)


async def _handle_dismiss_finding(arguments: dict[str, Any]) -> list[TextContent]:
    from filigree.mcp_server import _get_db

    args = _parse_args(arguments, DismissFindingArgs)
    finding_id = args.get("finding_id", "")
    if not isinstance(finding_id, str) or not finding_id.strip():
        return _text(ErrorResponse(error="finding_id is required", code="validation_error"))

    reason = args.get("reason")

    tracker = _get_db()
    try:
        updated = tracker.update_finding(finding_id, status="false_positive", dismiss_reason=reason or None)
    except KeyError:
        return _text(ErrorResponse(error=f"Finding not found: {finding_id}", code="not_found"))
    except (ValueError, sqlite3.Error) as e:
        return _text(ErrorResponse(error=str(e), code="validation_error"))
    return _text(updated)
