"""
Test Repository - Data access layer for test-related database operations.

This repository handles all SQL queries for tests, products, and sync metadata.
It maintains clean separation between business logic (PersistentCache) and data access.

MVP: Uses instance-level customer_id (single customer)
STORY-010: Will change to method-level customer_id (multi-customer)
STORY-023c: Extended with API refresh methods and bug joins
"""

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

import aiosqlite

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

# Import from top-level timezone_utils (minimal dependencies) to avoid circular imports
from testio_mcp.utilities.timezone_utils import normalize_to_utc

logger = logging.getLogger(__name__)


class TestRepository(BaseRepository):
    """Repository for test-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.

    Attributes:
        db: aiosqlite connection (initialized externally)
        client: TestIO API client for refresh operations (STORY-023c)
        customer_id: Stable customer identifier (MVP: instance-level)
    """

    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)

    # ============================================================================
    # Test Operations
    # ============================================================================

    async def test_exists(self, test_id: int, product_id: int) -> bool:
        """Check if test exists in database.

        Args:
            test_id: Test identifier
            product_id: Product identifier

        Returns:
            True if test exists, False otherwise
        """
        cursor = await self.db.execute(
            "SELECT 1 FROM tests WHERE id = ? AND customer_id = ? AND product_id = ? LIMIT 1",
            (test_id, self.customer_id, product_id),
        )
        result = await cursor.fetchone()
        return result is not None

    async def insert_test(self, test_data: dict[str, Any], product_id: int) -> None:
        """Insert or replace a single test in database.

        Args:
            test_data: Full test payload from API
            product_id: Product identifier
        """
        test_id = test_data.get("id")
        status = test_data.get("status", "unknown")

        # Normalize all timestamps to UTC before storing (STORY-021c)
        # This ensures consistent date comparisons regardless of API timezone variations
        created_at = normalize_to_utc(test_data.get("created_at"))
        start_at = normalize_to_utc(test_data.get("start_at"))
        end_at = normalize_to_utc(test_data.get("end_at"))

        await self.db.execute(
            """
            INSERT OR REPLACE INTO tests
            (id, customer_id, product_id, data, status, created_at, start_at, end_at, synced_at)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                test_id,
                self.customer_id,
                product_id,
                json.dumps(test_data),
                status,
                created_at,
                start_at,
                end_at,
                datetime.now(UTC).isoformat(),
            ),
        )
        await self.db.commit()

    async def update_test(self, test_data: dict[str, Any], product_id: int) -> None:
        """Update existing test with fresh data from API.

        Used by AC4 refresh logic to update test status and data.

        Args:
            test_data: Full test payload from API
            product_id: Product identifier
        """
        test_id = test_data.get("id")
        status = test_data.get("status", "unknown")

        # Normalize all timestamps to UTC before storing (STORY-021c)
        # This ensures consistent date comparisons regardless of API timezone variations
        start_at = normalize_to_utc(test_data.get("start_at"))
        end_at = normalize_to_utc(test_data.get("end_at"))

        await self.db.execute(
            """
            UPDATE tests
            SET data = ?, status = ?, start_at = ?, end_at = ?, synced_at = ?
            WHERE id = ? AND customer_id = ? AND product_id = ?
            """,
            (
                json.dumps(test_data),
                status,
                start_at,
                end_at,
                datetime.now(UTC).isoformat(),
                test_id,
                self.customer_id,
                product_id,
            ),
        )
        await self.db.commit()

    async def count_tests(self) -> int:
        """Count total tests for current customer.

        Returns:
            Total number of tests
        """
        cursor = await self.db.execute(
            "SELECT COUNT(*) FROM tests WHERE customer_id = ?", (self.customer_id,)
        )
        result = await cursor.fetchone()
        return result[0] if result else 0

    async def count_product_tests(self, product_id: int) -> int:
        """Count tests for a specific product.

        Args:
            product_id: Product identifier

        Returns:
            Number of tests for this product
        """
        cursor = await self.db.execute(
            "SELECT COUNT(*) FROM tests WHERE customer_id = ? AND product_id = ?",
            (self.customer_id, product_id),
        )
        result = await cursor.fetchone()
        return result[0] if result else 0

    async def get_product_last_synced(self, product_id: int) -> str | None:
        """Get last sync timestamp for a specific product.

        Args:
            product_id: Product identifier

        Returns:
            ISO 8601 timestamp of last sync, or None if never synced
        """
        cursor = await self.db.execute(
            "SELECT last_synced FROM products WHERE id = ? AND customer_id = ?",
            (product_id, self.customer_id),
        )
        result = await cursor.fetchone()
        return result[0] if result else None

    async def get_oldest_test_date(self) -> str | None:
        """Get oldest test end date for current customer.

        Returns:
            ISO 8601 timestamp of oldest test end date, or None if no tests
        """
        cursor = await self.db.execute(
            "SELECT MIN(end_at) FROM tests WHERE customer_id = ?", (self.customer_id,)
        )
        result = await cursor.fetchone()
        return result[0] if result and result[0] else None

    async def get_newest_test_date(self) -> str | None:
        """Get newest test end date for current customer.

        Returns:
            ISO 8601 timestamp of newest test end date, or None if no tests
        """
        cursor = await self.db.execute(
            "SELECT MAX(end_at) FROM tests WHERE customer_id = ?", (self.customer_id,)
        )
        result = await cursor.fetchone()
        return result[0] if result and result[0] else None

    async def delete_all_tests(self) -> None:
        """Delete all tests for current customer.

        Used by clear_database operation.
        """
        await self.db.execute("DELETE FROM tests WHERE customer_id = ?", (self.customer_id,))
        await self.db.commit()

    async def delete_product_tests(self, product_id: int) -> None:
        """Delete all tests for a specific product.

        Args:
            product_id: Product identifier
        """
        await self.db.execute(
            "DELETE FROM tests WHERE customer_id = ? AND product_id = ?",
            (self.customer_id, product_id),
        )
        await self.db.commit()

    async def get_mutable_tests(self, product_id: int) -> list[dict[str, Any]]:
        """Get tests that can change (mutable statuses only) (STORY-021g).

        Mutable test statuses (need frequent sync):
        - customer_finalized: Customer marked as final but can change
        - waiting: Waiting to start
        - running: Currently active, bugs being reported
        - locked: Finalized but not archived yet (still mutable!)
        - initialized: Created but not started

        Immutable test statuses (never change):
        - archived: Test completed and archived (final state)
        - cancelled: Test cancelled (final state)

        Args:
            product_id: Product identifier

        Returns:
            List of dicts with id, status, end_at (sorted by end_at DESC)
        """
        cursor = await self.db.execute(
            """
            SELECT id, status, end_at
            FROM tests
            WHERE customer_id = ?
              AND product_id = ?
              AND status IN ('customer_finalized', 'waiting', 'running', 'locked', 'initialized')
              AND status IS NOT NULL
            ORDER BY end_at DESC
            """,
            (self.customer_id, product_id),
        )
        rows = await cursor.fetchall()
        return [{"id": row[0], "status": row[1], "end_at": row[2]} for row in rows]

    async def query_tests(
        self,
        product_id: int,
        statuses: list[str] | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
        date_field: str = "start_at",
        page: int = 1,
        per_page: int = 100,
        offset: int = 0,
    ) -> list[dict[str, Any]]:
        """Query tests from database with filtering and pagination.

        Implements AC3 query interface with SQL-based filtering for performance.

        Args:
            product_id: Product identifier
            statuses: Optional list of status values to filter by
            start_date: Optional start date for date range filter
            end_date: Optional end date for date range filter
            date_field: Field to use for date filtering (start_at, end_at, created_at)
            page: Page number (1-indexed, default: 1)
            per_page: Number of results per page (default: 100)
            offset: Additional offset on top of page calculation (default: 0)
                   Final offset = offset + (page - 1) * per_page
                   Use case: Fetch small sample (page=1, per_page=10), then continue
                   with larger pages (page=1, per_page=100, offset=10)

        Returns:
            List of test dictionaries (deserialized from JSON data column)

        Raises:
            ValueError: If date_field is not in allowed list (SQL injection prevention)
        """
        # SECURITY: Validate date_field to prevent SQL injection
        ALLOWED_DATE_FIELDS = {"start_at", "end_at", "created_at"}
        if date_field not in ALLOWED_DATE_FIELDS:
            raise ValueError(
                f"Unsupported date_field: {date_field!r}. "
                f"Must be one of: {', '.join(sorted(ALLOWED_DATE_FIELDS))}"
            )

        # Build WHERE clause dynamically
        where_clauses = ["customer_id = ?", "product_id = ?"]
        params: list[Any] = [self.customer_id, product_id]

        # Status filter
        if statuses:
            placeholders = ",".join("?" * len(statuses))
            where_clauses.append(f"status IN ({placeholders})")
            params.extend(statuses)

        # Date range filter (safe after validation)
        if start_date or end_date:
            if start_date:
                where_clauses.append(f"json_extract(data, '$.{date_field}') >= ?")
                params.append(start_date.isoformat())
            if end_date:
                where_clauses.append(f"json_extract(data, '$.{date_field}') <= ?")
                params.append(end_date.isoformat())

        # Calculate pagination offset (combines page-based + additional offset)
        # Examples:
        # - Page-based only: page=2, per_page=50, offset=0 → items 50-99
        # - With offset: page=1, per_page=100, offset=10 → items 10-109
        actual_offset = offset + (page - 1) * per_page

        # Build final query
        # Sort by end_at DESC (most recently ended tests first)
        # Uses indexed column for efficient sorting
        query = f"""
            SELECT data
            FROM tests
            WHERE {" AND ".join(where_clauses)}
            ORDER BY end_at DESC
            LIMIT ? OFFSET ?
        """
        params.extend([per_page, actual_offset])

        # Execute query
        cursor = await self.db.execute(query, params)
        rows = await cursor.fetchall()

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

    async def count_filtered_tests(
        self,
        product_id: int,
        statuses: list[str] | None = None,
        start_date: datetime | None = None,
        end_date: datetime | None = None,
        date_field: str = "start_at",
    ) -> int:
        """Count tests matching query criteria (for pagination total_count).

        Uses same WHERE clause logic as query_tests() but returns count instead.

        Args:
            product_id: Product ID to filter by
            statuses: Optional list of status values to filter by
            start_date: Optional start date filter (inclusive)
            end_date: Optional end date filter (inclusive)
            date_field: Date field to filter on ("start_at", "end_at", "created_at", or "any")

        Returns:
            Total number of tests matching the criteria
        """
        # Build WHERE clause (same logic as query_tests)
        where_clauses = ["customer_id = ?", "product_id = ?"]
        params: list[Any] = [self.customer_id, product_id]

        # Status filter
        if statuses:
            placeholders = ", ".join("?" * len(statuses))
            where_clauses.append(f"status IN ({placeholders})")
            params.extend(statuses)

        # Date filters
        if start_date:
            if date_field == "any":
                where_clauses.append("(start_at >= ? OR end_at >= ? OR created_at >= ?)")
                params.extend([start_date.isoformat()] * 3)
            else:
                where_clauses.append(f"{date_field} >= ?")
                params.append(start_date.isoformat())

        if end_date:
            if date_field == "any":
                where_clauses.append("(start_at <= ? OR end_at <= ? OR created_at <= ?)")
                params.extend([end_date.isoformat()] * 3)
            else:
                where_clauses.append(f"{date_field} <= ?")
                params.append(end_date.isoformat())

        # Build count query
        query = f"""
            SELECT COUNT(*)
            FROM tests
            WHERE {" AND ".join(where_clauses)}
        """

        # Execute query
        cursor = await self.db.execute(query, params)
        result = await cursor.fetchone()
        return result[0] if result else 0

    async def get_test_with_bugs(self, test_id: int) -> dict[str, Any] | None:
        """Get test data with associated bugs (LEFT JOIN).

        Fetches test data from database and includes all associated bugs.
        Returns None if test doesn't exist.

        Args:
            test_id: Test identifier

        Returns:
            Dictionary with test data and bugs list, or None if not found:
                {
                    "test": {...},  # Deserialized test data
                    "bugs": [...]   # List of bug dicts
                }

        Note:
            This query assumes bugs have been refreshed via BugRepository.refresh_bugs().
            Background sync does NOT refresh bugs - they are always fetched on-demand.
        """
        # Get test data
        cursor = await self.db.execute(
            "SELECT data FROM tests WHERE id = ? AND customer_id = ?",
            (test_id, self.customer_id),
        )
        test_row = await cursor.fetchone()

        if not test_row:
            return None

        test_data = json.loads(test_row[0])

        # Get bugs for this test
        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),
        )
        bug_rows = await cursor.fetchall()
        bugs = [json.loads(row[0]) for row in bug_rows]

        return {"test": test_data, "bugs": bugs}

    async def refresh_test(self, test_id: int) -> None:
        """Fetch fresh test data from API and upsert to SQLite.

        Used by get operations to ensure data is always fresh.
        This supplements the background sync which discovers new tests.

        Args:
            test_id: Test identifier

        Raises:
            httpx.HTTPStatusError: If API request fails (including 404)

        Note:
            Background sync populates tests for list operations (~10ms).
            Refresh operations ensure get operations have fresh data (~70ms).
            Preserves bugs_synced_at for existing tests (STORY-024 fix).
        """
        # Fetch test from API
        # API endpoint: GET /exploratory_tests/{test_id}
        response = await self.client.get(f"exploratory_tests/{test_id}")
        test_data = response.get("exploratory_test", {})

        if not test_data:
            logger.warning(f"Test {test_id} returned empty data from API")
            return

        # Extract product_id from test data
        product_id = test_data.get("product", {}).get("id")
        if not product_id:
            logger.warning(f"Test {test_id} missing product_id, skipping upsert")
            return

        # STORY-024 fix: Preserve bugs_synced_at for existing tests
        # Use update_test (which preserves bugs_synced_at) for existing tests
        # Use insert_test for new tests (bugs_synced_at will be NULL until first bug fetch)
        if await self.test_exists(test_id, product_id):
            await self.update_test(test_data, product_id)
        else:
            await self.insert_test(test_data, product_id)

        logger.debug(f"Refreshed test {test_id} from API")

    # ============================================================================
    # Product Operations
    # ============================================================================

    async def count_products(self) -> int:
        """Count total products for current customer.

        Returns:
            Total number of products
        """
        cursor = await self.db.execute(
            "SELECT COUNT(*) FROM products WHERE customer_id = ?", (self.customer_id,)
        )
        result = await cursor.fetchone()
        return result[0] if result else 0

    async def get_product_info(self, product_id: int) -> dict[str, Any] | None:
        """Get product information from database.

        Args:
            product_id: Product identifier

        Returns:
            Product info dictionary with id, name, type, or None if not found

        Note:
            Provides fallback values when database has empty JSON (bug in cache.py:622).
            STORY-021a/LEO-51 will fix the empty JSON issue.
        """
        cursor = await self.db.execute(
            """
            SELECT
                p.id,
                p.data
            FROM products p
            WHERE p.customer_id = ? AND p.id = ?
            """,
            (self.customer_id, product_id),
        )
        row = await cursor.fetchone()

        if not row:
            return None

        # Parse JSON data column
        import json

        product_data = json.loads(row[1]) if row[1] else {}

        # Extract fields from data with fallback values (handles empty JSON gracefully)
        return {
            "id": row[0],
            "name": product_data.get("name") or f"Product {row[0]}",
            "type": product_data.get("type") or "unknown",
        }

    async def get_synced_products_info(self) -> list[dict[str, Any]]:
        """Get information about synced products for current customer.

        Returns:
            List of product info dictionaries with id, name, last_synced, test_count
        """
        cursor = await self.db.execute(
            """
            SELECT
                p.id,
                json_extract(p.data, '$.name') as name,
                p.last_synced,
                (
                    SELECT COUNT(*)
                    FROM tests t
                    WHERE t.product_id = p.id AND t.customer_id = p.customer_id
                ) as test_count
            FROM products p
            WHERE p.customer_id = ?
            ORDER BY p.last_synced DESC
            """,
            (self.customer_id,),
        )
        rows = await cursor.fetchall()

        result: list[dict[str, Any]] = [
            {
                "id": row[0],
                "name": row[1],
                "last_synced": row[2],
                "test_count": row[3],
            }
            for row in rows
        ]
        return result

    async def get_oldest_mutable_test_synced_at(self) -> str | None:
        """Get the oldest synced_at timestamp for mutable tests only.

        Mutable test statuses (need frequent sync):
        - customer_finalized: Customer marked as final but can change
        - waiting: Waiting to start
        - running: Currently active, bugs being reported
        - locked: Finalized but not archived yet
        - initialized: Created but not started

        Immutable statuses (archived, cancelled) are excluded as they never change.

        Returns:
            Timestamp string (YYYY-MM-DD HH:MM:SS) or None if no mutable tests
        """
        cursor = await self.db.execute(
            """
            SELECT MIN(synced_at) as oldest_sync
            FROM tests
            WHERE customer_id = ?
              AND status IN ('customer_finalized', 'waiting', 'running', 'locked', 'initialized')
            """,
            (self.customer_id,),
        )
        row = await cursor.fetchone()
        await cursor.close()

        return row[0] if row and row[0] else None

    async def update_product_last_synced(self, product_id: int) -> None:
        """Update product's last_synced timestamp without modifying data.

        Uses INSERT ... ON CONFLICT to either create a minimal row (if product
        doesn't exist) or update only the timestamp (if it exists).

        Args:
            product_id: Product identifier
        """
        now_utc = datetime.now(UTC).isoformat()
        await self.db.execute(
            """
            INSERT INTO products (id, customer_id, data, last_synced)
            VALUES (?, ?, '{}', ?)
            ON CONFLICT(id) DO UPDATE SET
                last_synced = ?
            WHERE id = excluded.id AND customer_id = excluded.customer_id
            """,
            (product_id, self.customer_id, now_utc, now_utc),
        )
        await self.db.commit()

    async def delete_all_products(self) -> None:
        """Delete all products for current customer.

        Used by clear_database operation.
        """
        await self.db.execute("DELETE FROM products WHERE customer_id = ?", (self.customer_id,))
        await self.db.commit()

    # ============================================================================
    # Sync Metadata Operations
    # ============================================================================

    async def get_problematic_tests(self, product_id: int | None = None) -> list[dict[str, Any]]:
        """Get tests that failed to sync due to API 500 errors.

        Args:
            product_id: Optional filter for specific product

        Returns:
            List of problematic test records with boundary information
        """
        cursor = await self.db.execute(
            "SELECT value FROM sync_metadata WHERE key = 'problematic_tests'"
        )
        result = await cursor.fetchone()

        if not result or not result[0]:
            return []

        problematic_tests: list[dict[str, Any]] = json.loads(result[0])

        # Filter by product_id if provided
        if product_id is not None:
            problematic_tests = [t for t in problematic_tests if t.get("product_id") == product_id]

        return problematic_tests

    async def log_problematic_test(self, test_info: dict[str, Any]) -> None:
        """Append problematic test info to sync_metadata.

        Args:
            test_info: Test information dict with boundary data
        """
        # Get existing problematic tests
        cursor = await self.db.execute(
            "SELECT value FROM sync_metadata WHERE key = 'problematic_tests'"
        )
        result = await cursor.fetchone()

        problematic_tests = []
        if result and result[0]:
            problematic_tests = json.loads(result[0])

        # Generate unique event_id if not provided
        if "event_id" not in test_info:
            test_info["event_id"] = str(uuid.uuid4())

        # Append new test
        problematic_tests.append(test_info)

        # Update metadata
        await self.db.execute(
            """
            INSERT OR REPLACE INTO sync_metadata (key, value)
            VALUES ('problematic_tests', ?)
            """,
            (json.dumps(problematic_tests),),
        )
        await self.db.commit()

        logger.warning(
            f"Logged problematic test: event_id={test_info.get('event_id')}, "
            f"product_id={test_info.get('product_id')}, "
            f"position_range={test_info.get('position_range')}, "
            f"recovery_attempts={test_info.get('recovery_attempts')}"
        )
