"""Bug transformers.

This module provides transformation logic for:
- Converting API bug data to ORM models (input)
- Converting service layer bug dicts to API models (output)

Includes parsing rejection reasons from comments.
"""

import json
from datetime import datetime
from typing import Any, TypedDict

from testio_mcp.schemas.constants import REJECTION_REASONS
from testio_mcp.schemas.dtos import ServiceBugDTO


class BugOrmData(TypedDict, total=False):
    """Type-safe dictionary for Bug ORM model creation.

    This TypedDict documents the structure for Bug(**data) creation.
    Fields are populated in two phases:

    Phase 1 - transform_api_bug_to_orm():
        id, title, severity, status, actual_result, expected_result,
        rejection_reason, steps, reported_at, raw_data

    Phase 2 - Repository enrichment (bug_repository.py):
        customer_id, test_id, test_feature_id, reported_by_user_id, synced_at

    All fields use total=False since they're added incrementally.
    """

    # Phase 1: From transformer
    id: int | None
    title: str
    severity: str | None
    status: str | None
    actual_result: str | None
    expected_result: str | None
    rejection_reason: str | None
    steps: str | None
    reported_at: datetime | None
    raw_data: str
    known: bool | None

    # Phase 2: Added by repository
    customer_id: int
    test_id: int
    test_feature_id: int | None
    reported_by_user_id: int | None
    synced_at: datetime


def _parse_rejection_reason(comments: list[dict[str, Any]] | None) -> str | None:
    """Parse rejection reason from bug comments.

    Iterates through comments to find a match against known rejection reasons.
    Returns the key of the first matching reason, or None if no match found.

    Args:
        comments: List of comment dictionaries from API

    Returns:
        Rejection reason key (e.g., "request_timeout", "intended_behavior") or None
    """
    if not comments:
        return None

    for comment in comments:
        body = comment.get("body", "")
        if not body:
            continue

        for reason in REJECTION_REASONS:
            if reason["default_comment"] in body:
                return reason["key"]

    return None


def transform_api_bug_to_orm(api_bug: dict[str, Any]) -> BugOrmData:
    """Transform API bug dictionary to ORM-compatible dictionary.

    Handles:
    - Field extraction and normalization
    - Rejection reason parsing from comments (STORY-067)
    - Rich text field cleaning (STORY-063)

    Args:
        api_bug: Raw bug dictionary from TestIO API

    Returns:
        BugOrmData TypedDict compatible with Bug ORM model
    """
    # Extract basic fields
    bug_id = api_bug.get("id")
    title = api_bug.get("title", "Untitled")
    severity = api_bug.get("severity")
    status = api_bug.get("status")

    # Extract result fields (STORY-063)
    actual_result = api_bug.get("actual_result")
    expected_result = api_bug.get("expected_result")

    # Trim whitespace and convert empty strings to None
    if actual_result is not None:
        actual_result = actual_result.strip() or None
    if expected_result is not None:
        expected_result = expected_result.strip() or None

    # Extract steps (array -> newline-separated string)
    steps_array = api_bug.get("steps")
    steps = "\n".join(steps_array) if steps_array and isinstance(steps_array, list) else None

    # Parse rejection reason (STORY-067)
    rejection_reason = None
    if status == "rejected":
        comments = api_bug.get("comments", [])
        rejection_reason = _parse_rejection_reason(comments)

    # Parse reported_at
    reported_at_str = api_bug.get("reported_at")
    reported_at = None
    if reported_at_str:
        try:
            from dateutil import parser

            reported_at = parser.isoparse(reported_at_str)
        except (ValueError, TypeError):
            pass  # Invalid datetime, leave as None

    # Extract known flag (STORY-070)
    known_val = api_bug.get("known", False)
    known = bool(known_val) if isinstance(known_val, bool) else False

    return {
        "id": bug_id,
        "title": title,
        "severity": severity,
        "status": status,
        "actual_result": actual_result,
        "expected_result": expected_result,
        "rejection_reason": rejection_reason,
        "steps": steps,
        "reported_at": reported_at,
        "raw_data": json.dumps(api_bug),
        "known": known,
    }


def extract_custom_report_data(
    api_bug: dict[str, Any],
    exclude_fields: set[str] | None = None,
) -> dict[str, Any] | None:
    """Extract and flatten custom report data from API bug response.

    Transforms report.content.data array [{key, value}, ...] into flat dict
    {"key1": "val1", ...} for efficient json_extract() queries.

    Args:
        api_bug: Raw bug dictionary from TestIO API
        exclude_fields: Field keys to exclude (e.g., large transcript fields).
            Defaults to settings.CUSTOM_REPORT_EXCLUDE_FIELDS.

    Returns:
        Dict with "report_data" (JSON string) and optional "configuration_id",
        or None if no report data.
    """
    report = api_bug.get("report")
    if not report or not isinstance(report, dict):
        return None
    content = report.get("content")
    if not content or not isinstance(content, dict):
        return None
    data = content.get("data")
    if not data or not isinstance(data, list):
        return None

    if exclude_fields is None:
        from testio_mcp.config import settings

        exclude_fields = set(settings.CUSTOM_REPORT_EXCLUDE_FIELDS)

    flat: dict[str, str] = {}
    for item in data:
        if isinstance(item, dict) and "key" in item:
            key = item["key"]
            if key not in exclude_fields:
                flat[key] = item.get("value", "")

    if not flat:
        return None

    result: dict[str, Any] = {"report_data": json.dumps(flat)}

    # Extract configuration_id if available
    config_id = report.get("custom_report_configuration_id")
    if config_id is not None:
        result["configuration_id"] = config_id

    return result


def normalize_bug_for_output(bug: dict[str, Any]) -> dict[str, Any]:
    """Normalize bug dict for user-facing output.

    Passes bug through ServiceBugDTO to apply field validators
    (e.g., status: forwarded → open), preserving all original fields.

    Args:
        bug: Bug dictionary from repository/service layer

    Returns:
        Normalized bug dictionary with user-facing values
    """
    dto = ServiceBugDTO(**bug)
    # Return original dict with normalized status
    return {**bug, "status": dto.status}


def normalize_bug_list_for_output(bugs: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """Batch normalize bug list for user-facing output.

    Args:
        bugs: List of bug dictionaries from repository/service layer

    Returns:
        List of normalized bug dictionaries
    """
    return [normalize_bug_for_output(bug) for bug in bugs]
