"""
Classification Pipeline - LLM-powered primitives classification.

Classifies reviews using the primitives taxonomy (MANNER, SPEED, VALUE_FOR_MONEY, etc.)
and stores results in detected_spans_v2.

Stages:
- fetch: Find reviews without classification
- classify: LLM-powered span extraction with primitives
- save: Store results to detected_spans_v2

Usage:
    pipeline = ClassificationPipeline()
    await pipeline.initialize()
    result = await pipeline.process({"business_id": "Go Karts Mar Menor", "limit": 100})
"""

from __future__ import annotations

import hashlib
import json
import logging
import re
import time
import unicodedata
import uuid
from collections import Counter
from datetime import datetime
from typing import TYPE_CHECKING, Any

from pipeline_core import (
    BasePipeline,
    DashboardConfig,
    DashboardSection,
    PipelineMetadata,
    PipelineResult as BasePipelineResult,
    StageResult,
    WidgetConfig,
)

from reviewiq_pipeline.config import Config
from reviewiq_pipeline.db.connection import DatabasePool
from reviewiq_pipeline.services.llm_client import LLMClient, LLMClientBase

if TYPE_CHECKING:
    import asyncpg

logger = logging.getLogger(__name__)

# Stage names
STAGE_NAMES = ["fetch", "classify", "save"]

# Primitives taxonomy - maps primitive to domain
PRIMITIVES_BY_DOMAIN = {
    "O": ["TASTE", "CRAFT", "FRESHNESS", "TEMPERATURE", "EFFECTIVENESS", "ACCURACY", "CONDITION", "CONSISTENCY"],
    "P": ["MANNER", "COMPETENCE", "ATTENTIVENESS", "COMMUNICATION"],
    "J": ["SPEED", "FRICTION", "RELIABILITY", "AVAILABILITY"],
    "E": ["CLEANLINESS", "COMFORT", "SAFETY", "AMBIANCE", "ACCESSIBILITY", "DIGITAL_UX"],
    "V": ["PRICE_LEVEL", "PRICE_FAIRNESS", "PRICE_TRANSPARENCY", "VALUE_FOR_MONEY"],
    "meta": ["HONESTY", "ETHICS", "PROMISES", "ACKNOWLEDGMENT", "RESPONSE_QUALITY", "RECOVERY",
             "RETURN_INTENT", "RECOMMEND", "RECOGNITION", "UNMAPPED", "NON_INFORMATIVE"],
}

# Flatten for lookup
ALL_PRIMITIVES = []
PRIMITIVE_TO_DOMAIN = {}
for domain, primitives in PRIMITIVES_BY_DOMAIN.items():
    for p in primitives:
        ALL_PRIMITIVES.append(p)
        PRIMITIVE_TO_DOMAIN[p] = domain

# Classification prompt
CLASSIFICATION_PROMPT = """You are a review classifier using primitive-based analysis.

## TASK
Extract semantic spans from customer reviews and classify each span to exactly ONE primitive.

## PRIMITIVES (use ONLY these)
### OUTPUT (O) - Product/Service Quality
- TASTE: Flavor quality (food/beverage)
- CRAFT: Skill of execution, craftsmanship
- FRESHNESS: How fresh/new the product is
- TEMPERATURE: Serving temperature
- EFFECTIVENESS: Does it work/achieve purpose
- ACCURACY: Correct execution of order
- CONDITION: State at delivery
- CONSISTENCY: Same quality each time

### PEOPLE (P) - Staff Interactions
- MANNER: Friendliness and warmth
- COMPETENCE: Knowledge and skill
- ATTENTIVENESS: Being present and responsive
- COMMUNICATION: Clarity and updates

### JOURNEY (J) - Process and Timing
- SPEED: How fast things happen
- FRICTION: Ease of process
- RELIABILITY: Dependable service
- AVAILABILITY: Access to service/staff

### ENVIRONMENT (E) - Physical/Digital Space
- CLEANLINESS: Hygiene and tidiness
- COMFORT: Physical ease
- SAFETY: Physical safety
- AMBIANCE: Overall mood/atmosphere
- ACCESSIBILITY: Ease of access
- DIGITAL_UX: Digital experience

### VALUE (V) - Cost and Worth
- PRICE_LEVEL: Absolute cost
- PRICE_FAIRNESS: Fair for what you get
- PRICE_TRANSPARENCY: Clear about costs
- VALUE_FOR_MONEY: Overall value assessment

### META - Trust and Sentiment
- HONESTY: Truthfulness
- ETHICS: Moral conduct
- PROMISES: Keeping commitments
- ACKNOWLEDGMENT: Recognizing issues
- RESPONSE_QUALITY: How business responds
- RECOVERY: Making amends
- RETURN_INTENT: Would come back
- RECOMMEND: Would suggest to others
- RECOGNITION: Customer acknowledgment
- UNMAPPED: Cannot classify (use sparingly)
- NON_INFORMATIVE: No actionable content

## RULES
1. Extract 1-5 spans per review (prefer fewer, larger spans about same topic)
2. Each span gets exactly ONE primitive (most specific match)
3. Valence: + (positive), - (negative), 0 (neutral), ± (mixed)
4. Intensity: 1 (low), 2 (moderate), 3 (high/extreme)
5. Detail: 1 (vague), 2 (some detail), 3 (specific/actionable)
6. Confidence: 0.0 to 1.0

## OUTPUT FORMAT (JSON only)
{
  "spans": [
    {
      "text": "exact text from review",
      "start": 0,
      "end": 25,
      "primitive": "MANNER",
      "valence": "+",
      "intensity": 2,
      "detail": 2,
      "confidence": 0.85,
      "entity": null,
      "entity_type": null
    }
  ]
}

Return valid JSON only, no markdown."""

# Non-informative detection
PURE_JUNK_RE = re.compile(
    r'^[\s\.\!\?\,\-\_\~\*\#\@]+$'
    r'|^[\U0001F300-\U0001F9FF\U0001FA00-\U0001FAFF\U00002600-\U000027BF\s\.\!\?]+$'
    r'|^(translated by google|traducido por google)[\.\s]*$',
    re.IGNORECASE
)


def is_non_informative(text: str) -> tuple[bool, str]:
    """Check if review is non-informative (skip LLM)."""
    if not text or not text.strip():
        return True, "empty"

    text = text.strip()

    if PURE_JUNK_RE.match(text):
        return True, "junk_pattern"

    # No alphanumeric content
    alpha_chars = sum(1 for c in text if unicodedata.category(c).startswith('L'))
    digit_chars = sum(1 for c in text if unicodedata.category(c).startswith('N'))
    if alpha_chars == 0 and digit_chars == 0:
        return True, "no_content"

    # Pure repetition
    tokens = text.split()
    if len(tokens) >= 3:
        unique_tokens = len(set(t.lower() for t in tokens))
        if unique_tokens == 1 and alpha_chars < 20:
            return True, "pure_repetition"

    return False, ""


def compute_review_hash(text: str) -> str:
    """Compute hash for review text (for deduplication)."""
    normalized = text.strip().lower()
    return hashlib.sha256(normalized.encode()).hexdigest()[:16]


class ClassificationPipeline(BasePipeline):
    """
    Classification Pipeline - LLM-powered primitives classification.

    Processes reviews through LLM to extract semantic spans and classify
    them using the primitives taxonomy.
    """

    def __init__(self, config: Config | None = None):
        """Initialize the pipeline."""
        self._config = config or Config()
        self._db: DatabasePool | None = None
        self._llm: LLMClientBase | None = None
        self._initialized = False

    @property
    def config(self) -> Config:
        """Get pipeline configuration."""
        return self._config

    @property
    def metadata(self) -> PipelineMetadata:
        """Get pipeline metadata."""
        return PipelineMetadata(
            id="classification",
            name="Primitives Classification Pipeline",
            description="LLM-powered span extraction and primitives classification. Processes reviews and stores results in detected_spans_v2.",
            version="1.0.0",
            stages=STAGE_NAMES,
            input_type="BusinessInput",
        )

    async def initialize(self) -> None:
        """Initialize database and LLM connections."""
        if self._initialized:
            return

        logger.info("Initializing Classification pipeline...")

        self._db = DatabasePool(self._config)
        await self._db.initialize()

        self._llm = LLMClient.create(self._config)
        self._llm.set_prompt(CLASSIFICATION_PROMPT)

        self._initialized = True
        logger.info("Classification pipeline initialized")

    async def close(self) -> None:
        """Close all connections."""
        if self._llm:
            await self._llm.close()
            self._llm = None

        if self._db:
            await self._db.close()
            self._db = None

        self._initialized = False
        logger.info("Classification pipeline closed")

    async def process(
        self,
        input_data: dict[str, Any],
        stages: list[str] | None = None,
    ) -> BasePipelineResult:
        """
        Process reviews through classification.

        Args:
            input_data: Must contain business_id OR job_id. Optional: limit, batch_size
            stages: List of stage names to run (default: all)

        Returns:
            BasePipelineResult with classification stats
        """
        await self.initialize()

        stages = stages or STAGE_NAMES
        stages_run: list[str] = []
        stage_results: dict[str, Any] = {}

        business_id = input_data.get("business_id")
        job_id = input_data.get("job_id")
        limit = input_data.get("limit", 100)
        batch_size = input_data.get("batch_size", 10)

        # Resolve business_id from job_id
        if not business_id and job_id:
            try:
                async with self._db.pool.acquire() as conn:
                    row = await conn.fetchrow(
                        "SELECT business_name FROM jobs WHERE job_id = $1",
                        uuid.UUID(job_id) if isinstance(job_id, str) else job_id,
                    )
                    if row and row["business_name"]:
                        business_id = row["business_name"]
                        logger.info(f"Resolved business_id '{business_id}' from job_id")
            except Exception as e:
                logger.warning(f"Failed to resolve business_id: {e}")

        if not business_id:
            return BasePipelineResult(
                pipeline_id="classification",
                stages_run=[],
                stage_results={},
                success=False,
                error="business_id is required (provide business_id or job_id)",
            )

        # Generate run_id for this execution
        run_id = uuid.uuid4()
        context = {
            "business_id": business_id,
            "job_id": job_id,
            "limit": limit,
            "batch_size": batch_size,
            "run_id": run_id,
            "reviews": [],
            "classified": [],
        }

        try:
            # Stage: Fetch unclassified reviews
            if "fetch" in stages:
                start = time.time()
                logger.info(f"Fetching unclassified reviews for {business_id}")

                try:
                    reviews = await self._fetch_unclassified(business_id, limit)
                    context["reviews"] = reviews
                    duration_ms = int((time.time() - start) * 1000)
                    stages_run.append("fetch")
                    stage_results["fetch"] = StageResult(
                        stage="fetch",
                        success=True,
                        data={"reviews_found": len(reviews)},
                        error=None,
                        duration_ms=duration_ms,
                    )
                    logger.info(f"Found {len(reviews)} unclassified reviews")
                except Exception as e:
                    logger.exception("Fetch failed")
                    return BasePipelineResult(
                        pipeline_id="classification",
                        stages_run=stages_run,
                        stage_results=stage_results,
                        success=False,
                        error=f"Fetch failed: {e}",
                    )

            # Stage: Classify reviews
            if "classify" in stages and context["reviews"]:
                start = time.time()
                logger.info(f"Classifying {len(context['reviews'])} reviews")

                try:
                    classified = await self._classify_reviews(
                        context["reviews"],
                        business_id,
                        batch_size,
                    )
                    context["classified"] = classified
                    duration_ms = int((time.time() - start) * 1000)
                    stages_run.append("classify")

                    total_spans = sum(len(c.get("spans", [])) for c in classified)
                    stage_results["classify"] = StageResult(
                        stage="classify",
                        success=True,
                        data={
                            "reviews_classified": len(classified),
                            "total_spans": total_spans,
                            "llm_cost_usd": self._llm.total_cost_usd if self._llm else 0,
                        },
                        error=None,
                        duration_ms=duration_ms,
                    )
                    logger.info(f"Classified {len(classified)} reviews, {total_spans} spans")
                except Exception as e:
                    logger.exception("Classification failed")
                    stage_results["classify"] = StageResult(
                        stage="classify",
                        success=False,
                        data={},
                        error=str(e),
                        duration_ms=int((time.time() - start) * 1000),
                    )

            # Stage: Save results
            if "save" in stages and context["classified"]:
                start = time.time()
                logger.info(f"Saving {len(context['classified'])} classifications")

                try:
                    saved_count = await self._save_classifications(
                        context["classified"],
                        business_id,
                        job_id,
                        run_id,
                    )
                    duration_ms = int((time.time() - start) * 1000)
                    stages_run.append("save")
                    stage_results["save"] = StageResult(
                        stage="save",
                        success=True,
                        data={"spans_saved": saved_count},
                        error=None,
                        duration_ms=duration_ms,
                    )
                    logger.info(f"Saved {saved_count} spans to detected_spans_v2")
                except Exception as e:
                    logger.exception("Save failed")
                    stage_results["save"] = StageResult(
                        stage="save",
                        success=False,
                        data={},
                        error=str(e),
                        duration_ms=int((time.time() - start) * 1000),
                    )

            return BasePipelineResult(
                pipeline_id="classification",
                stages_run=stages_run,
                stage_results=stage_results,
                success=all(stage_results.get(s, {}).get("success", False) for s in stages_run),
            )

        except Exception as e:
            logger.exception("Pipeline failed")
            return BasePipelineResult(
                pipeline_id="classification",
                stages_run=stages_run,
                stage_results=stage_results,
                success=False,
                error=str(e),
            )

    async def _fetch_unclassified(
        self,
        business_id: str,
        limit: int,
    ) -> list[dict[str, Any]]:
        """Fetch reviews that haven't been classified yet."""
        async with self._db.pool.acquire() as conn:
            # Get reviews from reviews_latest that don't have spans in detected_spans_v2
            rows = await conn.fetch(
                """
                SELECT
                    r.review_id,
                    r.business_id,
                    r.text AS review_text,
                    r.rating,
                    r.review_time
                FROM pipeline.reviews_latest r
                LEFT JOIN (
                    SELECT DISTINCT review_id, business_id
                    FROM pipeline.detected_spans_v2
                ) s ON s.review_id = r.review_id AND s.business_id = r.business_id
                WHERE r.business_id = $1
                    AND s.review_id IS NULL
                    AND r.text IS NOT NULL
                    AND LENGTH(r.text) > 0
                ORDER BY r.review_time DESC
                LIMIT $2
                """,
                business_id,
                limit,
            )

            return [
                {
                    "review_id": row["review_id"],
                    "business_id": row["business_id"],
                    "text": row["review_text"],
                    "rating": row["rating"] or 3,
                    "review_time": row["review_time"],
                }
                for row in rows
            ]

    async def _classify_reviews(
        self,
        reviews: list[dict[str, Any]],
        business_id: str,
        batch_size: int,
    ) -> list[dict[str, Any]]:
        """Classify reviews using LLM."""
        results = []

        for review in reviews:
            text = review.get("text", "")
            rating = review.get("rating", 3)

            # Check for non-informative
            is_junk, reason = is_non_informative(text)
            if is_junk:
                results.append({
                    "review_id": review["review_id"],
                    "business_id": business_id,
                    "text": text,
                    "rating": rating,
                    "spans": [{
                        "text": text,
                        "start": 0,
                        "end": len(text),
                        "primitive": "NON_INFORMATIVE",
                        "valence": "0",
                        "intensity": 1,
                        "detail": 1,
                        "confidence": 1.0,
                        "entity": None,
                        "entity_type": None,
                        "mode": reason,
                    }],
                    "review_hash": compute_review_hash(text),
                })
                continue

            # Classify with LLM
            try:
                user_prompt = f"Rating: {rating}/5\nText: {text}"
                response, metadata = await self._llm.classify(text)

                spans = response.get("spans", [])

                # Validate primitives
                for span in spans:
                    if span.get("primitive") not in ALL_PRIMITIVES:
                        span["primitive"] = "UNMAPPED"
                        span["unmapped_keywords"] = [span.get("primitive", "unknown")]

                results.append({
                    "review_id": review["review_id"],
                    "business_id": business_id,
                    "text": text,
                    "rating": rating,
                    "spans": spans,
                    "review_hash": compute_review_hash(text),
                    "model": metadata.get("model"),
                })

            except Exception as e:
                logger.warning(f"LLM classification failed for review {review['review_id']}: {e}")
                # Fallback to UNMAPPED
                results.append({
                    "review_id": review["review_id"],
                    "business_id": business_id,
                    "text": text,
                    "rating": rating,
                    "spans": [{
                        "text": text,
                        "start": 0,
                        "end": len(text),
                        "primitive": "UNMAPPED",
                        "valence": "0",
                        "intensity": 1,
                        "detail": 1,
                        "confidence": 0.0,
                        "entity": None,
                        "entity_type": None,
                        "mode": "llm_error",
                    }],
                    "review_hash": compute_review_hash(text),
                })

        return results

    async def _save_classifications(
        self,
        classifications: list[dict[str, Any]],
        business_id: str,
        job_id: str | None,
        run_id: uuid.UUID,
    ) -> int:
        """Save classification results to detected_spans_v2."""
        saved_count = 0
        config_version = f"primitives_v1_{datetime.utcnow().strftime('%Y%m%d')}"

        async with self._db.pool.acquire() as conn:
            # Get GBP path for business
            gbp_row = await conn.fetchrow(
                """
                SELECT gbp_category_path
                FROM jobs
                WHERE business_name = $1
                    AND gbp_category_path IS NOT NULL
                ORDER BY created_at DESC
                LIMIT 1
                """,
                business_id,
            )
            gbp_path = str(gbp_row["gbp_category_path"]) if gbp_row and gbp_row["gbp_category_path"] else "unknown"

            for classification in classifications:
                review_id = classification["review_id"]
                review_hash = classification.get("review_hash")
                model = classification.get("model")

                for span in classification.get("spans", []):
                    try:
                        await conn.execute(
                            """
                            INSERT INTO pipeline.detected_spans_v2 (
                                job_id, business_id, review_id, gbp_path, sector_code,
                                config_version, primitive, valence, intensity, detail, mode,
                                confidence, span_text, span_start, span_end,
                                unmapped_keywords, entity, entity_type,
                                model, review_hash, run_id, created_at
                            ) VALUES (
                                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
                                $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, NOW()
                            )
                            """,
                            uuid.UUID(job_id) if job_id else None,
                            business_id,
                            review_id,
                            gbp_path,
                            gbp_path.split(".")[0] if "." in gbp_path else gbp_path,
                            config_version,
                            span.get("primitive", "UNMAPPED"),
                            span.get("valence", "0"),
                            span.get("intensity", 1),
                            span.get("detail", 1),
                            span.get("mode"),
                            span.get("confidence", 0.5),
                            span.get("text", ""),
                            span.get("start", 0),
                            span.get("end", 0),
                            span.get("unmapped_keywords"),
                            span.get("entity"),
                            span.get("entity_type"),
                            model,
                            review_hash,
                            run_id,
                        )
                        saved_count += 1
                    except Exception as e:
                        logger.warning(f"Failed to save span: {e}")

        return saved_count

    def get_dashboard_config(self) -> DashboardConfig:
        """Get dashboard configuration."""
        return DashboardConfig(
            pipeline_id="classification",
            title="Classification Pipeline",
            description="Monitor classification progress and quality",
            sections=[
                DashboardSection(
                    id="stats",
                    title="Classification Stats",
                    widgets=[
                        WidgetConfig(
                            id="reviews_classified",
                            type="stat_card",
                            title="Reviews Classified",
                            grid={"x": 0, "y": 0, "w": 3, "h": 1},
                            config={"value_key": "reviews_classified"},
                        ),
                        WidgetConfig(
                            id="total_spans",
                            type="stat_card",
                            title="Total Spans",
                            grid={"x": 3, "y": 0, "w": 3, "h": 1},
                            config={"value_key": "total_spans"},
                        ),
                        WidgetConfig(
                            id="llm_cost",
                            type="stat_card",
                            title="LLM Cost",
                            grid={"x": 6, "y": 0, "w": 3, "h": 1},
                            config={"value_key": "llm_cost_usd", "format": "${value:.4f}"},
                        ),
                    ],
                ),
            ],
            default_time_range="7d",
            refresh_interval=60,
        )

    async def get_widget_data(
        self,
        widget_id: str,
        params: dict[str, Any],
    ) -> dict[str, Any]:
        """Get data for dashboard widgets."""
        await self.initialize()

        business_id = params.get("business_id")
        if not business_id:
            return {"error": "business_id required"}

        async with self._db.pool.acquire() as conn:
            row = await conn.fetchrow(
                """
                SELECT
                    COUNT(DISTINCT review_id) as reviews_classified,
                    COUNT(*) as total_spans
                FROM pipeline.detected_spans_v2
                WHERE business_id = $1
                """,
                business_id,
            )

            return {
                "reviews_classified": row["reviews_classified"] or 0,
                "total_spans": row["total_spans"] or 0,
                "llm_cost_usd": 0,  # Would need to track this
            }

    async def health_check(self) -> dict[str, Any]:
        """Check pipeline health."""
        await self.initialize()

        checks = {}
        healthy = True

        # Check database
        try:
            async with self._db.pool.acquire() as conn:
                await conn.fetchval("SELECT 1")
            checks["database"] = "ok"
        except Exception as e:
            checks["database"] = str(e)
            healthy = False

        # Check LLM
        try:
            if self._llm:
                checks["llm"] = f"{self._config.llm_provider}/{self._config.llm_model}"
            else:
                checks["llm"] = "not_initialized"
        except Exception as e:
            checks["llm"] = str(e)

        return {"healthy": healthy, "checks": checks}
