"""
Category Resolver Service

Resolves business categories to the deepest node in the GBP taxonomy.
Uses a multi-phase approach:
1. Exact match from Google's category
2. LLM matching when no exact match
3. Hierarchical LLM classification when no Google category

This is critical for the classification pipeline as it provides context
for understanding and categorizing reviews.
"""
import asyncio
import logging
from dataclasses import dataclass
from typing import Optional
import asyncpg

from .llm_client import LLMClient

log = logging.getLogger(__name__)


@dataclass
class ResolvedCategory:
    """Result of category resolution."""
    category_id: int
    path: str  # ltree path as string
    name: str
    level: int
    method: str  # 'exact', 'llm', 'hierarchical'
    confidence: float  # 0.0 - 1.0


class CategoryResolver:
    """
    Resolves business categories to GBP taxonomy nodes.

    Usage:
        resolver = CategoryResolver(db_pool, llm_client)

        # With Google category
        result = await resolver.resolve("Toy store")
        # -> ResolvedCategory(path="Retail.Stores.Toy_store", method="exact")

        # Without Google category (infer from name)
        result = await resolver.resolve(None, business_name="Pura Vida Hostel")
        # -> ResolvedCategory(path="Travel_Hospitality.Hotels.Hostel", method="hierarchical")
    """

    def __init__(self, pool: asyncpg.Pool, llm_client: Optional[LLMClient] = None):
        self.pool = pool
        self.llm = llm_client
        self._level1_cache: list[dict] = []
        self._level2_cache: dict[str, list[dict]] = {}
        self._level3_cache: dict[str, list[dict]] = {}

    async def resolve(
        self,
        google_category: Optional[str] = None,
        business_name: Optional[str] = None,
        business_address: Optional[str] = None
    ) -> Optional[ResolvedCategory]:
        """
        Resolve to the deepest taxonomy node.

        Args:
            google_category: Category from Google Maps (e.g., "Toy store")
            business_name: Business name for inference if no Google category
            business_address: Address for additional context

        Returns:
            ResolvedCategory or None if resolution failed
        """
        # Phase 1: Try exact match if we have Google category
        if google_category:
            result = await self._exact_match(google_category)
            if result:
                log.info(f"Exact match: '{google_category}' -> {result.path}")
                return result

            # Phase 2: LLM matching for Google category
            if self.llm:
                result = await self._llm_match(google_category)
                if result:
                    log.info(f"LLM match: '{google_category}' -> {result.path}")
                    return result

        # Phase 3: Hierarchical classification from business name
        if business_name and self.llm:
            result = await self._hierarchical_classify(
                business_name=business_name,
                business_address=business_address,
                google_category=google_category  # May be None or unmatched
            )
            if result:
                log.info(f"Hierarchical: '{business_name}' -> {result.path}")
                return result

        log.warning(f"Could not resolve category for: {google_category or business_name}")
        return None

    async def _exact_match(self, google_category: str) -> Optional[ResolvedCategory]:
        """Try exact match against taxonomy."""
        async with self.pool.acquire() as conn:
            # Try exact match (case-insensitive)
            row = await conn.fetchrow("""
                SELECT id, name, path::text as path, level
                FROM gbp_categories
                WHERE LOWER(name) = LOWER($1) AND level = 3
            """, google_category)

            if row:
                return ResolvedCategory(
                    category_id=row['id'],
                    path=row['path'],
                    name=row['name'],
                    level=row['level'],
                    method='exact',
                    confidence=1.0
                )

            # Try fuzzy match (contains)
            row = await conn.fetchrow("""
                SELECT id, name, path::text as path, level
                FROM gbp_categories
                WHERE LOWER(name) LIKE LOWER($1) AND level = 3
                ORDER BY length(name) ASC
                LIMIT 1
            """, f"%{google_category}%")

            if row:
                return ResolvedCategory(
                    category_id=row['id'],
                    path=row['path'],
                    name=row['name'],
                    level=row['level'],
                    method='exact',
                    confidence=0.9
                )

        return None

    async def _llm_match(self, google_category: str) -> Optional[ResolvedCategory]:
        """Use LLM to match Google category to taxonomy."""
        # Get candidate categories (level 3) that might match
        async with self.pool.acquire() as conn:
            # Get categories with similar words
            words = google_category.lower().split()
            conditions = " OR ".join([f"LOWER(name) LIKE '%{w}%'" for w in words if len(w) > 2])

            if not conditions:
                return None

            candidates = await conn.fetch(f"""
                SELECT id, name, path::text as path, level
                FROM gbp_categories
                WHERE ({conditions}) AND level = 3
                ORDER BY name
                LIMIT 20
            """)

            if not candidates:
                # Get random sample for LLM to choose from
                candidates = await conn.fetch("""
                    SELECT id, name, path::text as path, level
                    FROM gbp_categories
                    WHERE level = 3
                    ORDER BY RANDOM()
                    LIMIT 50
                """)

        if not candidates:
            return None

        # Ask LLM to pick best match
        candidate_list = "\n".join([f"- {c['name']} ({c['path']})" for c in candidates])

        prompt = f"""Given the Google Maps business category "{google_category}", select the BEST matching category from this taxonomy list.

Candidates:
{candidate_list}

Respond with ONLY the exact category name from the list, nothing else.
If none match well, respond with "NONE"."""

        response = await self.llm.complete(prompt, max_tokens=50)
        selected_name = response.strip().strip('"').strip("'")

        if selected_name == "NONE":
            return None

        # Find the selected category
        for c in candidates:
            if c['name'].lower() == selected_name.lower():
                return ResolvedCategory(
                    category_id=c['id'],
                    path=c['path'],
                    name=c['name'],
                    level=c['level'],
                    method='llm',
                    confidence=0.8
                )

        return None

    async def _hierarchical_classify(
        self,
        business_name: str,
        business_address: Optional[str] = None,
        google_category: Optional[str] = None
    ) -> Optional[ResolvedCategory]:
        """
        Walk down the taxonomy tree using LLM at each level.

        Level 1 (16 sectors) -> Level 2 (91 types) -> Level 3 (4034 categories)
        """
        context = f"Business: {business_name}"
        if business_address:
            context += f"\nAddress: {business_address}"
        if google_category:
            context += f"\nGoogle category hint: {google_category}"

        # Level 1: Select sector
        level1_categories = await self._get_level_categories(1)
        sector = await self._llm_select_category(
            context=context,
            categories=level1_categories,
            level_name="sector"
        )

        if not sector:
            return None

        # Level 2: Select business type within sector
        level2_categories = await self._get_level_categories(2, parent_path=sector['path'])
        business_type = await self._llm_select_category(
            context=context,
            categories=level2_categories,
            level_name="business type",
            parent=sector['name']
        )

        if not business_type:
            return None

        # Level 3: Select specific category
        level3_categories = await self._get_level_categories(3, parent_path=business_type['path'])
        specific = await self._llm_select_category(
            context=context,
            categories=level3_categories,
            level_name="specific category",
            parent=business_type['name']
        )

        if not specific:
            return None

        return ResolvedCategory(
            category_id=specific['id'],
            path=specific['path'],
            name=specific['name'],
            level=specific['level'],
            method='hierarchical',
            confidence=0.7
        )

    async def _get_level_categories(
        self,
        level: int,
        parent_path: Optional[str] = None
    ) -> list[dict]:
        """Get categories at a specific level, optionally filtered by parent."""
        cache_key = f"{level}:{parent_path or 'root'}"

        # Check cache
        if level == 1 and self._level1_cache:
            return self._level1_cache
        if level == 2 and parent_path in self._level2_cache:
            return self._level2_cache[parent_path]
        if level == 3 and parent_path in self._level3_cache:
            return self._level3_cache[parent_path]

        async with self.pool.acquire() as conn:
            if parent_path:
                rows = await conn.fetch("""
                    SELECT id, name, path::text as path, level
                    FROM gbp_categories
                    WHERE level = $1 AND path <@ $2::ltree
                    ORDER BY name
                """, level, parent_path)
            else:
                rows = await conn.fetch("""
                    SELECT id, name, path::text as path, level
                    FROM gbp_categories
                    WHERE level = $1
                    ORDER BY name
                """, level)

            result = [dict(r) for r in rows]

            # Cache results
            if level == 1:
                self._level1_cache = result
            elif level == 2 and parent_path:
                self._level2_cache[parent_path] = result
            elif level == 3 and parent_path:
                self._level3_cache[parent_path] = result

            return result

    async def _llm_select_category(
        self,
        context: str,
        categories: list[dict],
        level_name: str,
        parent: Optional[str] = None
    ) -> Optional[dict]:
        """Ask LLM to select best category from list."""
        if not categories:
            return None

        # If only one option, return it
        if len(categories) == 1:
            return categories[0]

        category_list = "\n".join([f"- {c['name']}" for c in categories])

        parent_context = f" within {parent}" if parent else ""

        prompt = f"""{context}

Select the most appropriate {level_name}{parent_context} for this business.

Options:
{category_list}

Respond with ONLY the exact category name from the list, nothing else."""

        response = await self.llm.complete(prompt, max_tokens=50)
        selected_name = response.strip().strip('"').strip("'")

        # Find the selected category
        for c in categories:
            if c['name'].lower() == selected_name.lower():
                return c

        # Fuzzy match if exact not found
        for c in categories:
            if selected_name.lower() in c['name'].lower() or c['name'].lower() in selected_name.lower():
                return c

        # Return first as fallback
        log.warning(f"LLM selected '{selected_name}' not in list, using first option")
        return categories[0] if categories else None


async def resolve_job_category(
    pool: asyncpg.Pool,
    llm_client: LLMClient,
    job_id: str,
    google_category: Optional[str],
    business_name: Optional[str],
    business_address: Optional[str] = None
) -> Optional[ResolvedCategory]:
    """
    Resolve and save category for a job.

    This is the main entry point for pre-flight category resolution.
    """
    resolver = CategoryResolver(pool, llm_client)
    result = await resolver.resolve(
        google_category=google_category,
        business_name=business_name,
        business_address=business_address
    )

    if result:
        # Save to database
        async with pool.acquire() as conn:
            await conn.execute("""
                UPDATE jobs
                SET gbp_category_id = $2,
                    gbp_category_path = $3::ltree,
                    category_resolution_method = $4,
                    updated_at = NOW()
                WHERE job_id = $1::uuid
            """, job_id, result.category_id, result.path, result.method)

        log.info(f"Job {job_id}: resolved category to {result.path} ({result.method})")

    return result
