"""Bug Repository - Data access layer for bug-related database operations.

This repository handles all SQL queries for bugs and API refresh operations.
Intelligent bug caching (STORY-024): Immutable tests (archived/cancelled) are
always served from cache. Mutable tests check staleness and refresh if needed.

Reference: STORY-023c (SQLite-First Foundation - Repository Layer)
Reference: STORY-024 (Intelligent Bug Caching)
"""

import asyncio
import json
import logging
from datetime import UTC, datetime
from typing import Any

import aiosqlite

from testio_mcp.client import TestIOClient
from testio_mcp.config import settings
from testio_mcp.repositories.base_repository import BaseRepository

logger = logging.getLogger(__name__)


class BugRepository(BaseRepository):
    """Repository for bug-related database operations.

    Handles pure SQL queries and API refresh operations with no business logic.
    All queries are scoped to a specific customer for data isolation.
    """

    def __init__(self, db: aiosqlite.Connection, client: TestIOClient, customer_id: int) -> None:
        """Initialize repository with database connection and API client.

        Args:
            db: Active aiosqlite connection
            client: TestIO API client for refresh operations
            customer_id: Stable customer identifier from TestIO system
        """
        super().__init__(db, client, customer_id)

    async def get_bugs(self, test_id: int) -> list[dict[str, Any]]:
        """Get all bugs for a test from SQLite.

        Args:
            test_id: Test identifier

        Returns:
            List of bug dictionaries (deserialized from raw_data JSON column)
        """
        cursor = await self.db.execute(
            """
            SELECT raw_data
            FROM bugs
            WHERE test_id = ? AND customer_id = ?
            ORDER BY created_at DESC
            """,
            (test_id, self.customer_id),
        )
        rows = await cursor.fetchall()

        # Deserialize JSON data
        bugs: list[dict[str, Any]] = [json.loads(row[0]) for row in rows]
        return bugs

    async def get_bug_stats(self, test_id: int) -> dict[str, Any]:
        """Get bug statistics (counts by status and severity).

        Args:
            test_id: Test identifier

        Returns:
            Dictionary with bug count aggregations:
                {
                    "total": 10,
                    "by_status": {"accepted": 5, "rejected": 2, "pending": 3},
                    "by_severity": {"critical": 2, "high": 3, "medium": 5},
                    "by_acceptance_state": {"accepted": 7, "rejected": 3}
                }
        """
        # Count by status
        cursor = await self.db.execute(
            """
            SELECT status, COUNT(*) as count
            FROM bugs
            WHERE test_id = ? AND customer_id = ?
            GROUP BY status
            """,
            (test_id, self.customer_id),
        )
        status_rows = await cursor.fetchall()
        by_status = {row[0]: row[1] for row in status_rows if row[0]}

        # Count by severity
        cursor = await self.db.execute(
            """
            SELECT severity, COUNT(*) as count
            FROM bugs
            WHERE test_id = ? AND customer_id = ?
            GROUP BY severity
            """,
            (test_id, self.customer_id),
        )
        severity_rows = await cursor.fetchall()
        by_severity = {row[0]: row[1] for row in severity_rows if row[0]}

        # Count by acceptance_state
        cursor = await self.db.execute(
            """
            SELECT acceptance_state, COUNT(*) as count
            FROM bugs
            WHERE test_id = ? AND customer_id = ?
            GROUP BY acceptance_state
            """,
            (test_id, self.customer_id),
        )
        acceptance_rows = await cursor.fetchall()
        by_acceptance_state = {row[0]: row[1] for row in acceptance_rows if row[0]}

        # Total count
        cursor = await self.db.execute(
            """
            SELECT COUNT(*) as total
            FROM bugs
            WHERE test_id = ? AND customer_id = ?
            """,
            (test_id, self.customer_id),
        )
        total_row = await cursor.fetchone()
        total = total_row[0] if total_row else 0

        return {
            "total": total,
            "by_status": by_status,
            "by_severity": by_severity,
            "by_acceptance_state": by_acceptance_state,
        }

    async def get_bugs_cached_or_refresh(
        self,
        test_ids: list[int],
        force_refresh: bool = False,
    ) -> tuple[dict[int, list[dict[str, Any]]], dict[str, Any]]:
        """Get bugs with intelligent caching based on test mutability.

        Batch-aware method: Pass single test ID or multiple for efficient batch processing.

        Decision Logic (per test, priority order):
        1. Check bugs_synced_at and test status from tests table
        2. If BUG_CACHE_BYPASS=true → mark for refresh (debug mode)
        3. If force_refresh=True → mark for refresh (user override)
        4. If BUG_CACHE_ENABLED=false → mark for refresh (global toggle)
        5. If bugs_synced_at IS NULL → mark for refresh (never synced!)
        6. If test is immutable (archived/cancelled) → use cache (bugs won't change)
        7. If test is mutable (locked/running/etc.) → check staleness
           - If stale (>TTL seconds) → mark for refresh
           - If fresh → use cache
        8. Batch refresh all tests marked for refresh (single API call per batch)
        9. Return bugs for all test IDs from SQLite

        Args:
            test_ids: List of test identifiers (single or multiple)
            force_refresh: Bypass cache and fetch from API for all tests (default: False)

        Returns:
            Tuple of (bugs_dict, cache_stats):
                - bugs_dict: Dictionary mapping test_id -> list of bug dicts
                  Example: {123: [{bug1}, {bug2}], 124: [{bug3}]}
                - cache_stats: Cache efficiency metrics dict with:
                  - total_tests: int
                  - cache_hits: int
                  - api_calls: int
                  - cache_hit_rate: float (0-100)
                  - breakdown: dict with decision category counts

        Performance:
            - Single test cache hit: ~10ms (SQLite query)
            - Batch (295 tests, 80% cache hit): ~12 seconds vs ~45 seconds (4x faster)
            - Immutable tests: Always cache hit (no API calls)

        Logging:
            - DEBUG: Per-test decisions (SQLite vs API, with reason)
            - INFO: Summary stats (cache hit rate, breakdown by category)
            Example logs:
                Test 123: SQLite (immutable (archived))
                Test 124: SQLite (mutable (running), fresh (0.5h))
                Test 125: API (mutable (running), stale (2.3h))
                Bug cache: 236/295 from SQLite (80.0% hit rate), 59 from API
                Breakdown: 236 immutable, 40 mutable fresh, 19 mutable stale
        """
        if not test_ids:
            return {}, {
                "total_tests": 0,
                "cache_hits": 0,
                "api_calls": 0,
                "cache_hit_rate": 0.0,
                "breakdown": {},
            }

        # 1. Bulk query: Get test statuses and bugs_synced_at for all test IDs
        placeholders = ",".join("?" * len(test_ids))
        cursor = await self.db.execute(
            f"""
            SELECT id, status, bugs_synced_at
            FROM tests
            WHERE id IN ({placeholders}) AND customer_id = ?
            """,
            (*test_ids, self.customer_id),
        )
        rows = await cursor.fetchall()
        test_metadata = {row[0]: {"status": row[1], "bugs_synced_at": row[2]} for row in rows}

        # 2. Determine which tests need refreshing
        tests_to_refresh: list[int] = []
        now = datetime.now(UTC)

        # Track decision stats for logging
        cache_decisions = {
            "immutable_cached": 0,
            "mutable_fresh": 0,
            "mutable_stale": 0,
            "never_synced": 0,
            "force_refresh": 0,
            "cache_disabled": 0,
            "cache_bypass": 0,
            "not_in_db": 0,
        }

        for test_id in test_ids:
            metadata = test_metadata.get(test_id)

            if not metadata:
                # Test not in DB - need to refresh (will likely 404 from API)
                tests_to_refresh.append(test_id)
                cache_decisions["not_in_db"] += 1
                logger.debug(f"Test {test_id}: API (not in database)")
                continue

            test_status = metadata["status"]
            bugs_synced_at_str = metadata["bugs_synced_at"]

            # Parse bugs_synced_at timestamp (ISO format with timezone)
            bugs_synced_at: datetime | None = None
            if bugs_synced_at_str:
                try:
                    bugs_synced_at = datetime.fromisoformat(bugs_synced_at_str)
                except ValueError:
                    logger.warning(f"Invalid bugs_synced_at for test {test_id}, will refresh")

            # Apply decision logic (same priority order as before)
            should_refresh = False
            decision_reason = ""

            if settings.BUG_CACHE_BYPASS:
                should_refresh = True
                decision_reason = "BUG_CACHE_BYPASS active"
                cache_decisions["cache_bypass"] += 1
            elif force_refresh:
                should_refresh = True
                decision_reason = "force_refresh=True"
                cache_decisions["force_refresh"] += 1
            elif not settings.BUG_CACHE_ENABLED:
                should_refresh = True
                decision_reason = "BUG_CACHE_ENABLED=false"
                cache_decisions["cache_disabled"] += 1
            elif bugs_synced_at is None:
                # Never synced - MUST fetch
                should_refresh = True
                decision_reason = "never synced"
                cache_decisions["never_synced"] += 1
            elif test_status in settings.IMMUTABLE_TEST_STATUSES:
                # Immutable (archived/cancelled) - use cache
                should_refresh = False
                decision_reason = f"immutable ({test_status})"
                cache_decisions["immutable_cached"] += 1
            elif test_status in settings.MUTABLE_TEST_STATUSES:
                # Mutable - check staleness
                seconds_since_sync = (now - bugs_synced_at).total_seconds()
                hours_since_sync = seconds_since_sync / 3600

                if seconds_since_sync > settings.BUG_CACHE_TTL_SECONDS:
                    should_refresh = True
                    decision_reason = f"mutable ({test_status}), stale ({hours_since_sync:.1f}h)"
                    cache_decisions["mutable_stale"] += 1
                else:
                    should_refresh = False
                    decision_reason = f"mutable ({test_status}), fresh ({hours_since_sync:.1f}h)"
                    cache_decisions["mutable_fresh"] += 1
            else:
                # Unknown status - default to refresh (defensive)
                should_refresh = True
                decision_reason = f"unknown status '{test_status}'"
                logger.warning(f"Test {test_id} has unknown status '{test_status}'")

            # Log decision
            source = "API" if should_refresh else "SQLite"
            logger.debug(f"Test {test_id}: {source} ({decision_reason})")

            if should_refresh:
                tests_to_refresh.append(test_id)

        # 3. Log cache efficiency summary
        cache_hits = len(test_ids) - len(tests_to_refresh)
        cache_hit_rate = (cache_hits / len(test_ids) * 100) if test_ids else 0

        logger.info(
            f"Bug cache decisions: {cache_hits}/{len(test_ids)} from SQLite "
            f"({cache_hit_rate:.1f}% hit rate), {len(tests_to_refresh)} from API"
        )

        # Log breakdown by category (non-zero only)
        breakdown_parts = []
        if cache_decisions["immutable_cached"]:
            breakdown_parts.append(f"{cache_decisions['immutable_cached']} immutable (cached)")
        if cache_decisions["mutable_fresh"]:
            breakdown_parts.append(f"{cache_decisions['mutable_fresh']} mutable fresh (cached)")
        if cache_decisions["mutable_stale"]:
            breakdown_parts.append(f"{cache_decisions['mutable_stale']} mutable stale (API)")
        if cache_decisions["never_synced"]:
            breakdown_parts.append(f"{cache_decisions['never_synced']} never synced (API)")
        if cache_decisions["force_refresh"]:
            breakdown_parts.append(f"{cache_decisions['force_refresh']} force refresh (API)")
        if cache_decisions["cache_disabled"]:
            breakdown_parts.append(f"{cache_decisions['cache_disabled']} cache disabled (API)")
        if cache_decisions["cache_bypass"]:
            breakdown_parts.append(f"{cache_decisions['cache_bypass']} cache bypass (API)")
        if cache_decisions["not_in_db"]:
            breakdown_parts.append(f"{cache_decisions['not_in_db']} not in DB (API)")

        if breakdown_parts:
            logger.info(f"Breakdown: {', '.join(breakdown_parts)}")

        # 4. Batch refresh tests that need it (existing method)
        if tests_to_refresh:
            # Batch API calls (15 tests per batch, using existing method)
            BATCH_SIZE = 15
            batches = [
                tests_to_refresh[i : i + BATCH_SIZE]
                for i in range(0, len(tests_to_refresh), BATCH_SIZE)
            ]
            await asyncio.gather(*[self.refresh_bugs_batch(batch) for batch in batches])

            # Bulk update bugs_synced_at
            await self._update_bugs_synced_at_batch(tests_to_refresh)

        # 5. Return bugs for ALL requested test IDs from SQLite
        result: dict[int, list[dict[str, Any]]] = {}
        for test_id in test_ids:
            result[test_id] = await self.get_bugs(test_id)

        # 6. Build cache stats for transparency
        cache_stats = {
            "total_tests": len(test_ids),
            "cache_hits": cache_hits,
            "api_calls": len(tests_to_refresh),
            "cache_hit_rate": cache_hit_rate,
            "breakdown": {
                k: v
                for k, v in cache_decisions.items()
                if v > 0  # Only non-zero counts
            },
        }

        return result, cache_stats

    async def _update_bugs_synced_at_batch(self, test_ids: list[int]) -> None:
        """Update bugs_synced_at timestamp for multiple tests (bulk update).

        Args:
            test_ids: List of test identifiers to update
        """
        if not test_ids:
            return

        # Use CASE statement for efficient bulk update
        placeholders = ",".join("?" * len(test_ids))
        now_utc = datetime.now(UTC).isoformat()
        await self.db.execute(
            f"""
            UPDATE tests
            SET bugs_synced_at = ?
            WHERE id IN ({placeholders}) AND customer_id = ?
            """,
            (now_utc, *test_ids, self.customer_id),
        )
        await self.db.commit()

    async def refresh_bugs(self, test_id: int) -> int:
        """Fetch fresh bugs from API, upsert to SQLite, return count.

        Deletes existing bugs for this test and inserts fresh data from API.
        This ensures bugs are always up-to-date.

        Args:
            test_id: Test identifier

        Returns:
            Number of bugs upserted

        Raises:
            httpx.HTTPStatusError: If API request fails
        """
        # Fetch bugs from API
        # API endpoint: GET /bugs?filter_test_cycle_ids={test_id}
        response = await self.client.get(f"bugs?filter_test_cycle_ids={test_id}")
        bugs_data: list[dict[str, Any]] = response.get("bugs", [])

        # Delete existing bugs for this test
        await self.db.execute(
            "DELETE FROM bugs WHERE test_id = ? AND customer_id = ?",
            (test_id, self.customer_id),
        )

        # Insert fresh bugs
        now_utc = datetime.now(UTC).isoformat()
        for bug in bugs_data:
            bug_id = bug.get("id")
            title = bug.get("title", "Untitled")
            severity = bug.get("severity")
            status = bug.get("status")
            acceptance_state = bug.get("acceptance_state")
            created_at = bug.get("created_at")

            await self.db.execute(
                """
                INSERT INTO bugs
                (id, customer_id, test_id, title, severity, status,
                 acceptance_state, created_at, raw_data, synced_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    bug_id,
                    self.customer_id,
                    test_id,
                    title,
                    severity,
                    status,
                    acceptance_state,
                    created_at,
                    json.dumps(bug),
                    now_utc,
                ),
            )

        await self.db.commit()

        logger.debug(f"Refreshed {len(bugs_data)} bugs for test {test_id}")
        return len(bugs_data)

    async def refresh_bugs_batch(self, test_ids: list[int]) -> dict[int, int]:
        """Fetch fresh bugs for multiple tests in a single API call.

        More efficient than calling refresh_bugs() individually when processing
        multiple tests (e.g., EBR generation with 295 tests).

        Args:
            test_ids: List of test identifiers (10-15 recommended for optimal batching)

        Returns:
            Dictionary mapping test_id -> bug_count

        Raises:
            httpx.HTTPStatusError: If API request fails

        Example:
            >>> counts = await repo.refresh_bugs_batch([123, 124, 125])
            >>> counts
            {123: 5, 124: 12, 125: 3}
        """
        if not test_ids:
            return {}

        # API supports comma-separated test IDs
        # GET /bugs?filter_test_cycle_ids=123,124,125
        test_ids_str = ",".join(str(tid) for tid in test_ids)
        response = await self.client.get(f"bugs?filter_test_cycle_ids={test_ids_str}")
        bugs_data: list[dict[str, Any]] = response.get("bugs", [])

        # Delete existing bugs for all tests in batch
        placeholders = ",".join("?" * len(test_ids))
        await self.db.execute(
            f"DELETE FROM bugs WHERE test_id IN ({placeholders}) AND customer_id = ?",
            (*test_ids, self.customer_id),
        )

        # Insert fresh bugs (grouped by test_id for return counts)
        bug_counts: dict[int, int] = dict.fromkeys(test_ids, 0)

        now_utc = datetime.now(UTC).isoformat()
        for bug in bugs_data:
            bug_id = bug.get("id")

            # Extract test_id from nested test object
            # Bug API response format: {"test": {"id": 141290, "title": "..."}}
            test_obj = bug.get("test", {})
            test_id = test_obj.get("id")

            title = bug.get("title", "Untitled")
            severity = bug.get("severity")
            status = bug.get("status")
            acceptance_state = bug.get("acceptance_state")
            created_at = bug.get("created_at")

            # Track count for this test
            if test_id in bug_counts:
                bug_counts[test_id] += 1

            await self.db.execute(
                """
                INSERT INTO bugs
                (id, customer_id, test_id, title, severity, status,
                 acceptance_state, created_at, raw_data, synced_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    bug_id,
                    self.customer_id,
                    test_id,
                    title,
                    severity,
                    status,
                    acceptance_state,
                    created_at,
                    json.dumps(bug),
                    now_utc,
                ),
            )

        await self.db.commit()

        logger.debug(
            f"Batch refreshed bugs for {len(test_ids)} tests: "
            f"total_bugs={len(bugs_data)}, counts={bug_counts}"
        )
        return bug_counts

    async def delete_bugs_for_test(self, test_id: int) -> None:
        """Delete all bugs for a specific test.

        Args:
            test_id: Test identifier
        """
        await self.db.execute(
            "DELETE FROM bugs WHERE test_id = ? AND customer_id = ?",
            (test_id, self.customer_id),
        )
        await self.db.commit()
