"""
Dynamic prompt builder for URT classification.

Fetches taxonomy from database to build the system prompt,
ensuring single source of truth and including examples.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    import asyncpg

logger = logging.getLogger(__name__)

# Entity extraction rules for staff recognition
ENTITY_EXTRACTION_RULES = """
## ENTITY EXTRACTION (Staff Recognition)

When a span mentions a SPECIFIC PERSON by name, extract:
- entity: The person's name exactly as written
- entity_type: "staff" for employees, "customer" for other people mentioned

### EXTRACT (set entity + entity_type):
- "Miglė was amazing" → entity: "Miglė", entity_type: "staff"
- "Thank you Carlos!" → entity: "Carlos", entity_type: "staff"
- "Adrian helped us" → entity: "Adrian", entity_type: "staff"
- "Ačiū Artūrui" → entity: "Artūrui", entity_type: "staff"
- "bartender Eivydas" → entity: "Eivydas", entity_type: "staff"
- "our server Maria" → entity: "Maria", entity_type: "staff"

### DO NOT EXTRACT (keep entity: null):
- "The bartender was rude" → no specific name, keep null
- "Staff was friendly" → generic reference, keep null
- "Service was great" → no person mentioned, keep null
- "The manager helped" → role only, no name, keep null

### Name Recognition Tips:
- Look for CAPITALIZED words that are NOT at sentence start
- Common patterns: "[Name] was/is [adjective]", "thank [Name]", "[role] [Name]"
- International names: Miglė, Eivydas, Žydrė, Artūras (Lithuanian), Carlos, María (Spanish), etc.
- When a name appears near: bartender, waiter, server, staff, manager, helped, thank, amazing, great, rude

IMPORTANT: When in doubt, extract the name. Staff recognition is valuable - false positives are acceptable.
"""

# Static parts of the prompt that don't change
PROMPT_HEADER = """You are a review classification system using URT (Universal Review Taxonomy) v5.1.

Your task is to extract semantic spans from customer reviews and classify each span independently.

## SPAN EXTRACTION RULES

**CRITICAL: Use TOPIC-BASED splitting, NOT sentence-based splitting.**

A span = all consecutive text about the SAME topic/domain, regardless of sentence count.

### When to KEEP TOGETHER (same span):
- Multiple sentences about the same topic: "The food was great. I loved the pasta. The sauce was perfect." → ONE span (all about Offering)
- Cause and effect: "The wait was long because they were understaffed" → ONE span
- Elaboration: "Staff was rude. They ignored us for 20 minutes." → ONE span (both about People)
- Single-topic reviews: Even if 5 sentences, if all about food → ONE span

### When to SPLIT (separate spans):
- Contrasting conjunctions that change topic: "Food was great BUT service was slow" → TWO spans
- Domain change: food (O) → staff (P) → ambiance (E) = split at each change
- Target change: "The waiter was nice but the manager was rude" → TWO spans (different people)

### Examples:
- "Amazing food. Best burger ever. Fries were crispy too." → 1 span (all Offering, V+)
- "Food was great but we waited an hour." → 2 spans (Offering V+, Journey V-)
- "I've been coming here for years. Always consistent quality." → 1 span (Relationship)
- "The staff are lovely and amazing with kids. More highchairs are definitely needed though." → 2 spans (People V+, Access V-)

**Guardrails**:
- Prefer FEWER, LARGER spans over many small ones
- Most reviews should have 1-3 spans, rarely more
- Min 1 span per review
- Spans must be non-overlapping

## CRITICAL CLASSIFICATION RULES (Common Mistakes to Avoid)

### RULE 1: Money/Price → ALWAYS use V codes (Value)
Any mention of: price, cost, fee, charge, €, $, deposit, refund, expensive, cheap, affordable
- ✅ "50€ extra" → V1.03 Hidden Costs
- ✅ "good price" → V1.01 Price Level
- ❌ NEVER use P codes for pricing (P is for People/staff behavior)

### RULE 2: Staff Behavior → ALWAYS use P codes (People)
Any mention of: friendly, rude, helpful, patient, amable, nett, simpático, attentive
- ✅ "staff was friendly" → P1.01 Warmth
- ✅ "rude employee" → P1.02 Respect
- ❌ NEVER use A codes for staff behavior (A is for Access/availability)

### RULE 3: Scam/Fraud/Deception → ALWAYS use R codes (Relationship)
Any mention of: scam, estafa, fraud, lied, cheat, dishonest, robbery, Abzocker
- ✅ "felt scammed" → R1.02 Ethics
- ✅ "they lied" → R1.01 Honesty
- ❌ NEVER use P or V codes for ethical issues

### RULE 4: Location/Finding → Use A codes (Access)
Difficulty finding a place, shuttle, meeting point, confusing directions
- ✅ "couldn't find shuttle" → A1.04 Wayfinding
- ✅ "far from airport" → A4.01 Location
- ❌ Don't confuse with J1.02 Punctuality (which is about being on time)

### RULE 5: Wait Time vs Punctuality
- J1.01 Speed = how FAST service is ("waited 2 hours", "slow service")
- J1.02 Punctuality = being ON TIME vs scheduled ("arrived late", "delayed")

"""

PROMPT_BATCH_OUTPUT_FORMAT = """
## BATCH OUTPUT FORMAT

When given multiple reviews, return a JSON object with a "reviews" array.
Each review in the array contains its own spans and summary.

{
  "reviews": [
    {
      "review_index": 0,
      "spans": [
        {
          "span_index": 0,
          "span_text": "exact text from this review",
          "span_start": 0,
          "span_end": 25,
          "urt_primary": "P1.01",
          "urt_secondary": [],
          "valence": "V+",
          "intensity": "I2",
          "specificity": "S2",
          "actionability": "A1",
          "temporal": "TC",
          "evidence": "ES",
          "comparative": "CR-N",
          "is_primary": true,
          "confidence": "high",
          "entity": "Maria",
          "entity_type": "staff",
          "usn": "URT:S:P1.01:+2:21TC.ES.N"
        }
      ],
      "review_summary": {
        "dominant_valence": "V+",
        "dominant_domain": "P",
        "span_count": 1,
        "has_comparative": false,
        "has_entity": true
      }
    },
    {
      "review_index": 1,
      "spans": [ ... ],
      "review_summary": { ... }
    }
  ]
}

CRITICAL RULES FOR BATCH PROCESSING:
1. Process each review INDEPENDENTLY - do not mix content between reviews
2. review_index MUST match the input order (0, 1, 2, ...)
3. span_start and span_end are relative to THAT review's text only
4. If you see the same entity (e.g., staff name "Maria") in multiple reviews, use consistent spelling
5. Output ALL reviews in the batch - never skip any
6. Each review must have at least 1 span
"""

PROMPT_SINGLE_OUTPUT_FORMAT = """
## SINGLE REVIEW OUTPUT FORMAT

Return valid JSON matching this schema. No markdown, no explanations.

{
  "spans": [
    {
      "span_index": 0,
      "span_text": "exact text from review",
      "span_start": 0,
      "span_end": 25,
      "urt_primary": "O1.01",
      "urt_secondary": [],
      "valence": "V+",
      "intensity": "I2",
      "specificity": "S2",
      "actionability": "A1",
      "temporal": "TC",
      "evidence": "ES",
      "comparative": "CR-N",
      "is_primary": true,
      "confidence": "high",
      "entity": null,
      "entity_type": null,
      "relation_type": null,
      "related_span_index": null,
      "usn": "URT:S:O1.01:+2:21TC.ES.N"
    }
  ],
  "review_summary": {
    "dominant_valence": "V+",
    "dominant_domain": "O",
    "span_count": 1,
    "has_comparative": false,
    "has_entity": false
  }
}
"""

PROMPT_DIMENSIONS = """
## DIMENSION CODES

### Valence
- V+ : Positive sentiment
- V- : Negative sentiment
- V0 : Neutral/factual
- V± : Mixed within the span

### Intensity
- I1 : Low ("okay", "fine", "decent")
- I2 : Moderate ("good", "bad", "slow")
- I3 : High ("amazing", "terrible", "unacceptable")

### Specificity
- S1 : Vague ("it was bad")
- S2 : Some detail ("the food was cold")
- S3 : Precise ("waited 45 minutes for appetizers")

### Actionability
- A1 : No clear action possible
- A2 : Possible actions, unclear which
- A3 : Clear, specific action ("train staff on X", "fix Y")

### Temporal
- TC : Current visit (default when no markers)
- TR : Recent pattern ("lately", "recently", "again")
- TH : Historical ("for years", "always", "used to")
- TF : Future ("won't return", "next time", "I expect")

### Evidence
- ES : Stated explicitly in text (default)
- EI : Inferred logically (not stated, but entailed)
- EC : Contextual (depends on surrounding text)

### Comparative
- CR-N : No comparison (default)
- CR-B : Better than alternatives
- CR-W : Worse than alternatives
- CR-S : Same as alternatives

## PRIMARY SPAN SELECTION

Mark exactly ONE span as is_primary=true using this order:
1. Highest intensity (I3 > I2 > I1)
2. Tie-break: negative over positive (V- > V± > V0 > V+)
3. Tie-break: earliest span_index

## USN (URT String Notation)

Generate a USN string for each span:
```
URT:S:{primary}[+{sec1}][+{sec2}]:{valence_sign}{intensity_num}:{S#}{A#}{temporal}.{evidence}.{CR_suffix}
```

Examples:
- `URT:S:J1.03:-2:22TC.ES.N` (J1.03, V-, I2, S2, A2, TC, ES, CR-N)
- `URT:S:P1.01+O2.03:+3:33TR.ES.B` (P1.01 primary, O2.03 secondary, V+, I3, S3, A3, TR, ES, CR-B)

Valence encoding: + for V+, - for V-, 0 for V0, ± for V±
CR suffix: N=CR-N, B=CR-B, W=CR-W, S=CR-S"""

# Domain-specific warnings to include
DOMAIN_WARNINGS = {
    "V": "USE FOR ALL PRICE/COST/FEE/MONEY MENTIONS",
    "P": "USE FOR STAFF BEHAVIOR ONLY, NOT PRICING",
}


class PromptBuilder:
    """
    Builds the classification prompt dynamically from database taxonomy.

    Usage:
        builder = PromptBuilder(db_pool)
        prompt = await builder.build()  # For single review
        prompt = await builder.build(batch_mode=True)  # For batch processing
    """

    def __init__(self, pool: asyncpg.Pool):
        self.pool = pool
        self._cached_prompt_single: str | None = None
        self._cached_prompt_batch: str | None = None
        self._cached_taxonomy: str | None = None

    async def build(self, force_refresh: bool = False, batch_mode: bool = False) -> str:
        """
        Build the complete system prompt from database taxonomy.

        Args:
            force_refresh: If True, rebuild even if cached
            batch_mode: If True, include batch output format

        Returns:
            Complete system prompt string
        """
        # Check if we can use cached version
        cache = self._cached_prompt_batch if batch_mode else self._cached_prompt_single
        if not force_refresh and cache:
            return cache

        # Build taxonomy section (shared between single and batch)
        if not self._cached_taxonomy or force_refresh:
            domains = await self._fetch_domains()
            subcodes = await self._fetch_subcodes()
            self._cached_taxonomy = self._build_taxonomy_section(domains, subcodes)
            logger.info(f"Built taxonomy section with {len(subcodes)} subcodes")

        # Combine all parts with appropriate output format
        output_format = PROMPT_BATCH_OUTPUT_FORMAT if batch_mode else PROMPT_SINGLE_OUTPUT_FORMAT
        prompt = (
            PROMPT_HEADER
            + self._cached_taxonomy
            + ENTITY_EXTRACTION_RULES
            + PROMPT_DIMENSIONS
            + output_format
        )

        # Cache it
        if batch_mode:
            self._cached_prompt_batch = prompt
        else:
            self._cached_prompt_single = prompt

        logger.info(f"Built {'batch' if batch_mode else 'single'} classification prompt")
        return prompt

    async def build_cacheable_parts(self) -> tuple[str, str]:
        """
        Build the prompt split into cacheable (static) and dynamic parts.

        For prompt caching, we want to separate:
        - Static part (taxonomy, rules) - can be cached
        - Dynamic part (output format) - varies by mode

        Returns:
            Tuple of (cacheable_prefix, suffix_for_batch)
        """
        if not self._cached_taxonomy:
            domains = await self._fetch_domains()
            subcodes = await self._fetch_subcodes()
            self._cached_taxonomy = self._build_taxonomy_section(domains, subcodes)

        # Static cacheable prefix (same for all calls)
        cacheable_prefix = (
            PROMPT_HEADER
            + self._cached_taxonomy
            + ENTITY_EXTRACTION_RULES
            + PROMPT_DIMENSIONS
        )

        return cacheable_prefix, PROMPT_BATCH_OUTPUT_FORMAT

    async def _fetch_domains(self) -> list[dict[str, Any]]:
        """Fetch domain definitions from database."""
        query = """
            SELECT code, name, description
            FROM pipeline.urt_domains
            ORDER BY code
        """
        rows = await self.pool.fetch(query)
        return [dict(row) for row in rows]

    async def _fetch_subcodes(self) -> list[dict[str, Any]]:
        """Fetch subcode definitions with examples from database."""
        query = """
            SELECT
                code,
                name,
                definition,
                positive_example,
                negative_example
            FROM pipeline.urt_subcodes
            ORDER BY code
        """
        rows = await self.pool.fetch(query)
        return [dict(row) for row in rows]

    def _build_taxonomy_section(
        self,
        domains: list[dict[str, Any]],
        subcodes: list[dict[str, Any]]
    ) -> str:
        """Build the taxonomy section of the prompt."""
        # Group subcodes by domain
        subcodes_by_domain: dict[str, list[dict]] = {}
        for subcode in subcodes:
            domain_code = subcode["code"][0]
            if domain_code not in subcodes_by_domain:
                subcodes_by_domain[domain_code] = []
            subcodes_by_domain[domain_code].append(subcode)

        # Build the section
        lines = ["## URT TAXONOMY (Use EXACT codes from database)", ""]

        for domain in domains:
            code = domain["code"]
            name = domain["name"]
            desc = domain["description"]
            domain_subcodes = subcodes_by_domain.get(code, [])

            # Domain header with warning if applicable
            warning = DOMAIN_WARNINGS.get(code, "")
            if warning:
                lines.append(f"### {code} - {name.upper()} ({len(domain_subcodes)} codes) ⚠️ {warning}")
            else:
                lines.append(f"### {code} - {name.upper()} ({len(domain_subcodes)} codes)")

            # Add each subcode with definition and examples
            for sc in domain_subcodes:
                sc_code = sc["code"]
                sc_name = sc["name"]
                sc_def = sc["definition"] or sc_name
                pos_ex = sc.get("positive_example")
                neg_ex = sc.get("negative_example")

                # Main line: code, name, definition
                line = f"{sc_code} {sc_name}: {sc_def}"

                # Add examples if available (helps LLM distinguish)
                if pos_ex and neg_ex:
                    line += f' [+"{pos_ex}" / -"{neg_ex}"]'
                elif pos_ex:
                    line += f' [+"{pos_ex}"]'
                elif neg_ex:
                    line += f' [-"{neg_ex}"]'

                lines.append(line)

            lines.append("")  # Blank line between domains

        return "\n".join(lines)

    def invalidate_cache(self) -> None:
        """Invalidate the cached prompt, forcing rebuild on next call."""
        self._cached_prompt_single = None
        self._cached_prompt_batch = None
        self._cached_taxonomy = None


# Global prompt cache for when DB is not available
_static_prompt_cache: str | None = None


async def build_prompt_from_db(pool: asyncpg.Pool) -> str:
    """
    Convenience function to build prompt from database.

    Args:
        pool: Database connection pool

    Returns:
        Complete system prompt
    """
    builder = PromptBuilder(pool)
    return await builder.build()


def get_static_fallback_prompt() -> str:
    """
    Get a static fallback prompt when database is not available.
    This should only be used in testing or when DB connection fails.
    """
    global _static_prompt_cache
    if _static_prompt_cache is None:
        # Import the hardcoded version as fallback
        from reviewiq_pipeline.services.llm_client import SYSTEM_PROMPT
        _static_prompt_cache = SYSTEM_PROMPT
    return _static_prompt_cache
