"""SQLite JSON1-based form cache for BPA form schemas.

Caches BPA form data locally with TTL-based expiry. Supports selective
component queries via json_tree() to avoid loading full form schemas.
"""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any

from mcp_eregistrations_bpa.db.connection import get_connection

FORM_CACHE_TTL_SECONDS = int(os.getenv("MCP_FORM_CACHE_TTL_SECONDS", "1800"))


async def get_cached_form(
    service_id: str, form_type: str, db_path: Path
) -> dict[str, Any] | None:
    """Get cached form_data if not expired. Returns None on miss/expiry."""
    async with get_connection(db_path) as conn:
        cursor = await conn.execute(
            """
            SELECT form_data FROM form_cache
            WHERE service_id = ? AND form_type = ?
              AND expires_at > datetime('now')
            """,
            (service_id, form_type),
        )
        row = await cursor.fetchone()
        if row is None:
            return None
        result: dict[str, Any] = json.loads(row[0])
        return result


async def put_form_cache(
    service_id: str, form_type: str, form_data: dict[str, Any], db_path: Path
) -> None:
    """Cache form_data with TTL. Uses INSERT OR REPLACE. Parses formSchema."""
    form_data_json = json.dumps(form_data)

    # Parse formSchema for queryable storage
    raw_schema = form_data.get("formSchema", "{}")
    if isinstance(raw_schema, str):
        form_schema = json.loads(raw_schema)
    else:
        form_schema = raw_schema
    form_schema_json = json.dumps(form_schema)

    async with get_connection(db_path) as conn:
        await conn.execute(
            """
            INSERT OR REPLACE INTO form_cache
                (service_id, form_type, form_data, form_schema, cached_at, expires_at)
            VALUES (?, ?, ?, ?, datetime('now'),
                    datetime('now', '+' || ? || ' seconds'))
            """,
            (
                service_id,
                form_type,
                form_data_json,
                form_schema_json,
                FORM_CACHE_TTL_SECONDS,
            ),
        )
        await conn.commit()


async def invalidate_form_cache(
    service_id: str, form_type: str, db_path: Path | None = None
) -> None:
    """Delete cache entry for a specific (service_id, form_type)."""
    async with get_connection(db_path) as conn:
        await conn.execute(
            "DELETE FROM form_cache WHERE service_id = ? AND form_type = ?",
            (service_id, form_type),
        )
        await conn.commit()


async def invalidate_all_forms(service_id: str, db_path: Path) -> None:
    """Delete all form cache entries for a service."""
    async with get_connection(db_path) as conn:
        await conn.execute(
            "DELETE FROM form_cache WHERE service_id = ?",
            (service_id,),
        )
        await conn.commit()


async def query_form_components(
    service_id: str,
    form_type: str,
    db_path: Path,
    component_key: str | None = None,
    component_type: str | None = None,
    required_only: bool = False,
    has_determinants: bool = False,
    search_label: str | None = None,
    path_prefix: str | None = None,
    visible_only: bool = False,
) -> list[dict[str, Any]] | None:
    """Query cached form components using SQLite JSON1. Returns None if not cached."""
    async with get_connection(db_path) as conn:
        # First check if we have a valid cache entry
        cursor = await conn.execute(
            """
            SELECT form_schema FROM form_cache
            WHERE service_id = ? AND form_type = ?
              AND expires_at > datetime('now')
            """,
            (service_id, form_type),
        )
        row = await cursor.fetchone()
        if row is None:
            return None

        form_schema_json = row[0]

        # Use json_tree to recursively walk the component tree
        # json_tree returns all nodes; we filter for objects that have a 'key' property
        # which identifies Form.io components.
        # We also fetch each component's unique path (parent path + array index)
        # for ancestor hidden propagation.
        params: list[str] = [form_schema_json]

        base_query = """
            SELECT jt.value as component_json,
                   jt.path || '[' || jt.key || ']' as json_path
            FROM json_tree(?, '$.components') AS jt
            WHERE jt.type = 'object'
              AND json_extract(jt.value, '$.key') IS NOT NULL
              AND json_extract(jt.value, '$.type') IS NOT NULL
        """

        if component_key is not None:
            base_query += " AND json_extract(jt.value, '$.key') = ?"
            params.append(component_key)

        if component_type is not None:
            base_query += " AND json_extract(jt.value, '$.type') = ?"
            params.append(component_type)

        if required_only:
            base_query += " AND json_extract(jt.value, '$.validate.required') = 1"

        if has_determinants:
            base_query += (
                " AND json_extract(jt.value, '$.determinantIds') IS NOT NULL"
                " AND json_extract(jt.value, '$.determinantIds') != '[]'"
            )

        if search_label is not None:
            base_query += " AND LOWER(json_extract(jt.value, '$.label')) LIKE ?"
            params.append(f"%{search_label.lower()}%")

        if path_prefix is not None:
            base_query += " AND jt.path LIKE ?"
            params.append(f"%.{path_prefix}%")

        cursor = await conn.execute(base_query, params)
        rows = await cursor.fetchall()

        # Pass 1: collect all components with their json_tree paths
        raw_components: list[tuple[dict[str, Any], str]] = []
        seen_keys: set[str] = set()
        for r in rows:
            comp = json.loads(r[0])
            json_path = r[1]
            key = comp.get("key")
            # Deduplicate: json_tree can return the same component at multiple
            # nesting levels (parent object contains child objects)
            if key and key not in seen_keys:
                seen_keys.add(key)
                raw_components.append((comp, json_path))

        # Pass 2: build hidden ancestor paths from ALL components in the schema
        # (not just filtered results — a hidden ancestor may not match the filter)
        hidden_cursor = await conn.execute(
            """
            SELECT jt.path || '[' || jt.key || ']' as json_path
            FROM json_tree(?, '$.components') AS jt
            WHERE jt.type = 'object'
              AND json_extract(jt.value, '$.hidden') = 1
              AND json_extract(jt.value, '$.key') IS NOT NULL
            """,
            (form_schema_json,),
        )
        hidden_rows = await hidden_cursor.fetchall()
        hidden_paths: list[str] = [r[0] for r in hidden_rows]

        def _has_hidden_ancestor(json_path: str) -> bool:
            """Check if any ancestor in the json_tree path was hidden."""
            for hp in hidden_paths:
                if json_path != hp and json_path.startswith(hp):
                    return True
            return False

        # Pass 3: build simplified output
        components: list[dict[str, Any]] = []
        for comp, json_path in raw_components:
            is_hidden = bool(comp.get("hidden")) or _has_hidden_ancestor(json_path)

            if visible_only and is_hidden:
                continue

            simplified: dict[str, Any] = {
                "key": comp.get("key"),
                "type": comp.get("type"),
                "label": comp.get("label"),
            }
            if comp.get("validate", {}).get("required"):
                simplified["required"] = True
            if comp.get("determinantIds"):
                simplified["determinant_ids"] = comp["determinantIds"]
            if is_hidden:
                simplified["hidden"] = True
            if comp.get("disabled"):
                simplified["disabled"] = True
            if comp.get("defaultValue") is not None:
                simplified["default_value"] = comp["defaultValue"]
            components.append(simplified)

        return components
