"""MCP tool for generating Executive Bug Reports (EBR).

This module implements the generate_ebr_report tool following the service
layer pattern (ADR-006). The tool is a thin wrapper that:
1. Validates input with Pydantic
2. Extracts dependencies from server context (ADR-007)
3. Delegates to MultiTestReportService (STORY-023e)
4. Converts exceptions to user-friendly error format
"""

from datetime import datetime
from typing import Annotated, Any, Literal

from fastmcp import Context
from fastmcp.exceptions import ToolError
from pydantic import BaseModel, Field, field_validator, model_validator

from testio_mcp.exceptions import ProductNotFoundException, TestIOAPIError
from testio_mcp.server import mcp
from testio_mcp.services.multi_test_report_service import MultiTestReportService
from testio_mcp.utilities import get_service
from testio_mcp.utilities.date_utils import parse_flexible_date
from testio_mcp.utilities.schema_utils import inline_schema_refs

# Type aliases for valid values (using Literal to avoid $defs in JSON schema)

TestStatus = Literal[
    "running", "locked", "archived", "cancelled", "customer_finalized", "initialized"
]

# Pydantic Models
# NOTE: Models use nested BaseModel classes for type safety and better FastAPI docs.
# Schemas are post-processed with inline_schema_refs() to avoid $ref resolution issues
# in some MCP clients like Gemini CLI 0.16.0.


class GenerateEBRInput(BaseModel):
    """Input validation for generate_ebr_report tool.

    Validates:
    1. Per-field: Rejects ambiguous year-only inputs (e.g., "2025")
    2. Cross-field: Validates date range (start_date <= end_date) after parsing
    """

    product_id: int = Field(gt=0)
    start_date: str | None = None
    end_date: str | None = None
    statuses: str | list[str] | None = None
    force_refresh_bugs: bool = False
    output_file: str | None = None

    @field_validator("start_date", "end_date", mode="before")
    @classmethod
    def validate_date_format(cls, v: str | None) -> str | None:
        """Validate date format and reject ambiguous year-only inputs.

        This runs BEFORE cross-field validation, catching year-only inputs
        like "2025" with a clear error message.

        Args:
            v: Date string value

        Returns:
            Original date string if valid (or None)

        Raises:
            ValueError: If date is year-only format (ambiguous)
        """
        if v is None:
            return v

        # Validate using parse_flexible_date (includes year-only check)
        # This will raise ToolError for invalid formats, which we convert to ValueError
        try:
            parse_flexible_date(v, start_of_day=True)
            return v
        except ToolError as e:
            # Convert ToolError to ValueError so Pydantic can handle it
            raise ValueError(str(e)) from e

    @model_validator(mode="after")
    def validate_date_range(self) -> "GenerateEBRInput":
        """Validate that start_date <= end_date after parsing flexible formats.

        Raises:
            ValueError: If start_date > end_date or date parsing fails
        """
        if self.start_date and self.end_date:
            # Parse dates using flexible parser
            try:
                parsed_start = parse_flexible_date(self.start_date, start_of_day=True)
                parsed_end = parse_flexible_date(self.end_date, start_of_day=False)

                # Convert to datetime for comparison
                start_dt = datetime.fromisoformat(parsed_start.replace("Z", "+00:00"))
                end_dt = datetime.fromisoformat(parsed_end.replace("Z", "+00:00"))

                # Validate start <= end
                if start_dt > end_dt:
                    raise ValueError(
                        f"start_date is after end_date: "
                        f"start_date='{self.start_date}' ({parsed_start}) > "
                        f"end_date='{self.end_date}' ({parsed_end}). "
                        f"Ensure start_date comes before or equals end_date."
                    )
            except ToolError as e:
                # Convert ToolError to ValueError so Pydantic can handle it
                raise ValueError(str(e)) from e

        return self


class BugCounts(BaseModel):
    """Bug classification counts for a test."""

    active_accepted: int = Field(
        description="Bugs actively accepted by customer (not auto-accepted)"
    )
    auto_accepted: int = Field(description="Bugs auto-accepted after 10 days")
    rejected: int = Field(description="Bugs rejected by customer")
    open: int = Field(description="Open bugs (forwarded to customer)")
    total_accepted: int = Field(description="Total accepted bugs (active + auto)", ge=0)
    reviewed: int = Field(
        description="Human-reviewed bugs (active + rejected, excludes auto)",
        ge=0,
    )


class TestBugMetrics(BaseModel):
    """Bug metrics for a single test in EBR."""

    __test__ = False

    test_id: int = Field(description="Test ID (integer from API)")
    title: str = Field(description="Test title")
    status: str = Field(description="Test lifecycle status (running, locked, archived, etc.)")
    start_at: str | None = Field(description="Test start date (ISO 8601)")
    end_at: str | None = Field(description="Test end date (ISO 8601)")
    bugs_count: int = Field(description="Total number of bugs for this test", ge=0)
    bugs: BugCounts = Field(description="Bug classification counts")
    active_acceptance_rate: float | None = Field(
        default=None,
        description="Active acceptance rate: active_accepted / total_bugs",
        ge=0.0,
        le=1.0,
    )
    auto_acceptance_rate: float | None = Field(
        default=None,
        description="Auto rate: auto_accepted / (active + auto). None if no accepted bugs",
        ge=0.0,
        le=1.0,
    )
    overall_acceptance_rate: float | None = Field(
        default=None,
        description="Overall acceptance rate: (active + auto) / total_bugs",
        ge=0.0,
        le=1.0,
    )
    rejection_rate: float | None = Field(
        default=None,
        description="Rejection rate: rejected / total_bugs",
        ge=0.0,
        le=1.0,
    )
    review_rate: float | None = Field(
        default=None,
        description=(
            "Review rate: (active_accepted + rejected) / total_bugs. "
            "Percentage of bugs reviewed by humans (excludes auto_accepted)"
        ),
        ge=0.0,
        le=1.0,
    )


class EBRSummary(BaseModel):
    """Summary metrics aggregated across all tests."""

    total_tests: int = Field(description="Total number of tests in report", ge=0, examples=[15])
    tests_by_status: dict[str, int] = Field(
        description="Count of tests grouped by status (running, locked, archived, etc.)",
        examples=[{"locked": 8, "archived": 5, "running": 2}],
    )
    statuses_applied: list[str] | str = Field(
        description=(
            "Test statuses included in this report. "
            "List of statuses if filtered, 'all' if no filter applied. "
            "Default: ['running', 'locked', 'archived', 'customer_finalized'] "
            "(excludes 'initialized' and 'cancelled')"
        ),
        examples=[["locked", "archived", "running"]],
    )
    total_bugs: int = Field(description="Total bugs across all tests", ge=0, examples=[342])
    bugs_by_status: dict[str, int] = Field(
        description="Bug counts by classification (active_accepted, auto_accepted, rejected, open)",
        examples=[{"active_accepted": 215, "auto_accepted": 48, "rejected": 63, "open": 16}],
    )
    total_accepted: int = Field(description="Total accepted bugs (active + auto)", ge=0)
    reviewed: int = Field(
        description="Total human-reviewed bugs (active + rejected, excludes auto)",
        ge=0,
    )
    active_acceptance_rate: float | None = Field(
        default=None,
        description="Active acceptance rate: active_accepted / total_bugs",
        ge=0.0,
        le=1.0,
    )
    auto_acceptance_rate: float | None = Field(
        default=None,
        description="Auto rate: auto_accepted / (active + auto). None if no accepted bugs",
        ge=0.0,
        le=1.0,
    )
    overall_acceptance_rate: float | None = Field(
        default=None,
        description="Overall acceptance rate: (active + auto) / total_bugs",
        ge=0.0,
        le=1.0,
    )
    rejection_rate: float | None = Field(
        default=None,
        description="Rejection rate: rejected / total_bugs",
        ge=0.0,
        le=1.0,
    )
    review_rate: float | None = Field(
        default=None,
        description=(
            "Overall review rate: (active_accepted + rejected) / total_bugs. "
            "Percentage reviewed by humans (excludes auto_accepted)"
        ),
        ge=0.0,
        le=1.0,
    )
    period: str = Field(description="Period covered by this report")


class CacheStats(BaseModel):
    """Bug cache efficiency statistics."""

    total_tests: int = Field(description="Total number of tests processed", ge=0, examples=[15])
    cache_hits: int = Field(
        description="Number of tests served from SQLite cache", ge=0, examples=[12]
    )
    api_calls: int = Field(description="Number of tests fetched from API", ge=0, examples=[3])
    cache_hit_rate: float = Field(
        description="Percentage of tests served from cache (0-100)",
        ge=0.0,
        le=100.0,
        examples=[80.0],
    )
    breakdown: dict[str, int] = Field(
        description=(
            "Breakdown of cache decisions by category. Keys: immutable_cached, "
            "mutable_fresh, mutable_stale, never_synced, force_refresh, "
            "cache_disabled, cache_bypass, not_in_db"
        ),
        examples=[
            {
                "immutable_cached": 8,
                "mutable_fresh": 4,
                "mutable_stale": 2,
                "never_synced": 1,
            }
        ],
    )


class GenerateEBROutput(BaseModel):
    """Executive Bug Report output.

    This model supports two response formats:
    1. Full report (when output_file is None): Contains summary, by_test, and cache_stats
    2. File export metadata (when output_file is specified): Contains file_path, summary,
       record_count, file_size_bytes, and format

    All fields are optional to accommodate both response types in a single schema.
    """

    # Full report fields (present when output_file is None)
    summary: EBRSummary = Field(description="Aggregate metrics across all tests")
    by_test: list[TestBugMetrics] | None = Field(
        default=None,
        description=(
            "Per-test bug metrics and acceptance rates (omitted when output_file is specified)"
        ),
    )
    cache_stats: CacheStats | None = Field(
        default=None,
        description=(
            "Cache efficiency metrics showing how many tests were served from SQLite "
            "vs fetched from API. High cache hit rate (>80%) indicates fast report "
            "generation. Use force_refresh_bugs=true if fresh data is needed. "
            "(Omitted when output_file is specified)"
        ),
    )

    # File export fields (present when output_file is specified)
    file_path: str | None = Field(
        default=None,
        description="Absolute path to exported file (only present when output_file is specified)",
    )
    record_count: int | None = Field(
        default=None,
        description="Number of tests in exported file (only present when output_file is specified)",
        ge=0,
    )
    file_size_bytes: int | None = Field(
        default=None,
        description="File size in bytes (only present when output_file is specified)",
        ge=0,
    )
    format: Literal["json"] | None = Field(
        default=None,
        description="File format (only present when output_file is specified)",
    )


# MCP Tool
@mcp.tool(output_schema=inline_schema_refs(GenerateEBROutput.model_json_schema()))
async def generate_ebr_report(
    product_id: Annotated[
        int,
        Field(
            gt=0,
            description="Product ID from TestIO (e.g., 598). Use list_products to find IDs.",
        ),
    ],
    ctx: Context,
    start_date: Annotated[
        str | None,
        Field(
            description=(
                "Start date for filtering tests. "
                "Use full ISO dates like '2025-07-01', or business terms like 'last 30 days'. "
                "DO NOT use year-only values like '2025' (ambiguous). "
                "If both start_date and end_date are provided, start_date must be <= end_date."
            ),
            json_schema_extra={
                "examples": [
                    "2025-07-01",  # ISO 8601 - REQUIRED format for specific dates
                    "2024-01-01",  # ISO 8601
                    "last 30 days",  # Business term
                    "this quarter",  # Business term
                    "yesterday",  # Business term
                ]
            },
        ),
    ] = None,
    end_date: Annotated[
        str | None,
        Field(
            description=(
                "End date for filtering tests. "
                "Use full ISO dates like '2025-10-31', or business terms like 'today'. "
                "DO NOT use year-only values like '2025' (ambiguous). "
                "If both start_date and end_date are provided, end_date must be >= start_date."
            ),
            json_schema_extra={
                "examples": [
                    "2025-10-31",  # ISO 8601 - REQUIRED format for specific dates
                    "2024-12-31",  # ISO 8601
                    "today",  # Business term
                    "yesterday",  # Business term
                ]
            },
        ),
    ] = None,
    statuses: Annotated[
        str | list[TestStatus] | None,
        Field(
            description=(
                "Filter tests by lifecycle status. "
                'Pass as comma-separated string: statuses="running,locked" '
                'or JSON array: statuses=["running", "locked"]. '
                "Valid values: running, locked, archived, cancelled, "
                "customer_finalized, initialized. "
                "**Default (None): Excludes 'initialized' and 'cancelled' "
                "(only executed tests). Pass explicit list to override.**"
            ),
            json_schema_extra={
                "examples": [
                    "locked",  # Completed tests only (string)
                    "running,locked",  # Active and completed (comma-separated)
                    ["locked"],  # Completed tests only (array)
                    ["running", "locked"],  # Active and completed (array)
                ]
            },
        ),
    ] = None,
    force_refresh_bugs: Annotated[
        bool,
        Field(
            description=(
                "Bypass cache and fetch all bugs from API (default: false). "
                "Set to true when you need guaranteed fresh bug data. "
                "WARNING: Significantly slower (4x) for large reports."
            )
        ),
    ] = False,
    output_file: Annotated[
        str | None,
        Field(
            description=(
                "Optional path to export full report as file instead of JSON response. "
                "Useful for large products (>100 tests) to avoid token limits. "
                "Absolute paths are used as-is. Relative paths are relative to "
                "~/.testio-mcp/reports/. Parent directories are created automatically. "
                "Supported formats: .json (MVP). If specified, returns file metadata "
                "instead of full data. If None, returns full JSON response (may truncate "
                "for large products)."
            ),
            json_schema_extra={
                "examples": [
                    "canva-q3-2025.json",  # Relative path
                    "q3-2025/canva.json",  # Subdirectory
                    "/tmp/reports/canva-q3-2025.json",  # Absolute path
                ]
            },
        ),
    ] = None,
) -> dict[str, Any]:
    """Analyze quality trends across multiple tests (reporting, metrics, acceptance rates).

    Returns aggregated bug metrics with acceptance rates and per-test summaries. Use for
    quarterly reviews, sprint retrospectives, and quality trend analysis. For troubleshooting
    specific tests, use get_test_status.

    **Date Validation:**
    - Year-only inputs (e.g., '2025') are rejected as ambiguous. Use ISO format ('2025-01-01')
      or business terms ('this year', 'last year') instead.
    - If both start_date and end_date are provided, start_date must be <= end_date.
    - Flexible formats supported: ISO 8601, business terms, relative dates.

    **Default Filtering:** By default, excludes 'initialized' (not reviewed/executed) and
    'cancelled' (never executed) tests. Only executed tests are included in metrics.
    Pass explicit statuses parameter to override this behavior.

    **File Export:** For large products (>100 tests), use `output_file` parameter to export
    the full report to a file instead of returning JSON response. This avoids token limits
    and provides a complete report. File is written to ~/.testio-mcp/reports/ by default
    (or absolute path if specified). Returns file metadata (path, size, summary) instead of
    full data when `output_file` is specified.

    Uses intelligent caching: immutable tests (archived/cancelled) cached in SQLite,
    mutable tests (running/locked) refreshed if stale (>1h). Typical execution: 10-30s
    for 100+ tests. Use force_refresh_bugs=true for guaranteed fresh data (slower).
    """
    # Validate input with Pydantic (including date range validation)
    # Convert TestStatus Literal types to strings for Pydantic model
    statuses_for_validation: str | list[str] | None = None
    if statuses is not None:
        if isinstance(statuses, str):
            statuses_for_validation = statuses
        else:
            # Convert list of Literal types to list of strings
            statuses_for_validation = [str(s) for s in statuses]

    try:
        validated_input = GenerateEBRInput(
            product_id=product_id,
            start_date=start_date,
            end_date=end_date,
            statuses=statuses_for_validation,
            force_refresh_bugs=force_refresh_bugs,
            output_file=output_file,
        )
    except ValueError as e:
        # Pydantic validation error (including date range validation)
        error_msg = str(e)
        if "start_date is after end_date" in error_msg:
            raise ToolError(
                f"❌ Invalid date range: {error_msg}\n"
                f"ℹ️  The start date must come before or equal to the end date\n"
                f"💡 For single-day reports, use the same date for both parameters. "
                f"For date ranges, ensure start_date < end_date."
            ) from e
        # Other validation errors
        raise ToolError(
            f"❌ Invalid input: {error_msg}\n"
            f"ℹ️  Check your parameter values\n"
            f"💡 Ensure all parameters are in the correct format"
        ) from e

    # Create service instance using helper
    service = get_service(ctx, MultiTestReportService)

    # Parse comma-separated string to list if needed (AI-friendly format)
    statuses_list: list[str] | None = None
    if validated_input.statuses is not None:
        if isinstance(validated_input.statuses, str):
            # Parse comma-separated string: "running,locked" -> ["running", "locked"]
            statuses_list = [s.strip() for s in validated_input.statuses.split(",")]
        else:
            # Extract enum values and convert to list of strings
            from enum import Enum

            statuses_list = [
                s.value if isinstance(s, Enum) else s for s in validated_input.statuses
            ]

    # Delegate to service and convert exceptions to MCP error format
    try:
        service_result = await service.generate_ebr_report(
            product_id=product_id,
            start_date=start_date,
            end_date=end_date,
            statuses=statuses_list,
            force_refresh_bugs=force_refresh_bugs,
            output_file=output_file,
        )

        # If output_file is specified, service returns file metadata
        if output_file is not None:
            # Service already wrote file and returned metadata
            # Return file export response (by_test and cache_stats are None)
            output = GenerateEBROutput(
                summary=EBRSummary(
                    total_tests=service_result["summary"]["total_tests"],
                    tests_by_status=service_result["summary"]["tests_by_status"],
                    statuses_applied=service_result["summary"]["statuses_applied"],
                    total_bugs=service_result["summary"]["total_bugs"],
                    bugs_by_status=service_result["summary"]["bugs_by_status"],
                    total_accepted=service_result["summary"]["total_accepted"],
                    reviewed=service_result["summary"]["reviewed"],
                    active_acceptance_rate=service_result["summary"].get("active_acceptance_rate"),
                    auto_acceptance_rate=service_result["summary"].get("auto_acceptance_rate"),
                    overall_acceptance_rate=service_result["summary"].get(
                        "overall_acceptance_rate"
                    ),
                    rejection_rate=service_result["summary"].get("rejection_rate"),
                    review_rate=service_result["summary"].get("review_rate"),
                    period=service_result["summary"]["period"],
                ),
                by_test=None,  # Omitted for file export
                cache_stats=None,  # Omitted for file export
                file_path=service_result["file_path"],
                record_count=service_result["record_count"],
                file_size_bytes=service_result["file_size_bytes"],
                format=service_result["format"],
            )
            return output.model_dump(by_alias=True, exclude_none=True)

        # Otherwise, transform service result to tool output format
        summary = service_result["summary"]
        by_test = service_result["by_test"]
        cache_stats = service_result["cache_stats"]

        # Build validated output
        output = GenerateEBROutput(
            summary=EBRSummary(
                total_tests=summary["total_tests"],
                tests_by_status=summary["tests_by_status"],
                statuses_applied=summary["statuses_applied"],
                total_bugs=summary["total_bugs"],
                bugs_by_status=summary["bugs_by_status"],
                total_accepted=summary["total_accepted"],
                reviewed=summary["reviewed"],
                active_acceptance_rate=summary.get("active_acceptance_rate"),
                auto_acceptance_rate=summary.get("auto_acceptance_rate"),
                overall_acceptance_rate=summary.get("overall_acceptance_rate"),
                rejection_rate=summary.get("rejection_rate"),
                review_rate=summary.get("review_rate"),
                period=summary["period"],
            ),
            cache_stats=CacheStats(
                total_tests=cache_stats["total_tests"],
                cache_hits=cache_stats["cache_hits"],
                api_calls=cache_stats["api_calls"],
                cache_hit_rate=cache_stats["cache_hit_rate"],
                breakdown=cache_stats["breakdown"],
            ),
            by_test=[
                TestBugMetrics(
                    test_id=test["test_id"],
                    title=test["title"],
                    status=test["status"],
                    start_at=test.get("start_at"),
                    end_at=test.get("end_at"),
                    bugs_count=test["bugs_count"],
                    bugs=BugCounts(
                        active_accepted=test["bugs"]["active_accepted"],
                        auto_accepted=test["bugs"]["auto_accepted"],
                        rejected=test["bugs"]["rejected"],
                        open=test["bugs"]["open"],
                        total_accepted=test["bugs"]["total_accepted"],
                        reviewed=test["bugs"]["reviewed"],
                    ),
                    active_acceptance_rate=test.get("active_acceptance_rate"),
                    auto_acceptance_rate=test.get("auto_acceptance_rate"),
                    overall_acceptance_rate=test.get("overall_acceptance_rate"),
                    rejection_rate=test.get("rejection_rate"),
                    review_rate=test.get("review_rate"),
                )
                for test in by_test
            ],
            # File export fields are None for full report
            file_path=None,
            record_count=None,
            file_size_bytes=None,
            format=None,
        )

        return output.model_dump(by_alias=True, exclude_none=True)

    except ProductNotFoundException as e:
        # Convert domain exception to ToolError with user-friendly message
        raise ToolError(
            f"❌ Product ID '{e.product_id}' not found\n"
            f"ℹ️  This product may not exist or you don't have access to it\n"
            f"💡 Use the list_products tool to see available products"
        ) from e

    except TestIOAPIError as e:
        # Convert API error to ToolError with user-friendly message
        raise ToolError(
            f"❌ API error: {e.message}\n"
            f"ℹ️  HTTP status code: {e.status_code}\n"
            f"💡 Check API status and try again. If the problem persists, contact support."
        ) from e

    except (PermissionError, OSError) as e:
        # File write errors (permissions, disk full, etc.)
        error_type = "permission" if isinstance(e, PermissionError) else "I/O"
        raise ToolError(
            f"❌ File export failed: {error_type} error\n"
            f"ℹ️  Cannot write to file: {str(e)}\n"
            f"💡 Check file permissions and disk space. "
            f"For relative paths, ensure ~/.testio-mcp/reports/ is writable."
        ) from e

    except ValueError as e:
        # Path validation errors (invalid path, unsupported extension, path traversal)
        if "path" in str(e).lower() or "extension" in str(e).lower():
            raise ToolError(
                f"❌ Invalid output file path\n"
                f"ℹ️  {str(e)}\n"
                f"💡 Use absolute paths or relative paths under ~/.testio-mcp/reports/. "
                f"Supported extensions: .json"
            ) from e
        # Re-raise other ValueError (e.g., from date parsing)
        raise

    except ToolError:
        # Re-raise ToolError from parse_flexible_date (already formatted)
        raise

    except Exception as e:
        # Catch-all for unexpected errors
        raise ToolError(
            f"❌ Unexpected error: {str(e)}\n"
            f"ℹ️  An unexpected error occurred while generating EBR\n"
            f"💡 Please try again or contact support if the problem persists"
        ) from e
