"""
LLM client abstraction supporting OpenAI and Anthropic.

Provides a unified interface for classification requests with:
- Provider abstraction (OpenAI/Anthropic)
- Structured output (JSON mode)
- Retry handling
- Cost tracking
"""

from __future__ import annotations

import json
import logging
import time
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from reviewiq_pipeline.config import Config
    from reviewiq_pipeline.contracts import LLMClassificationResponse

logger = logging.getLogger(__name__)

# System prompt for URT classification
SYSTEM_PROMPT = """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

## URT TAXONOMY - COMPLETE (138 codes, use EXACT codes)

### O - OFFERING (Product/Service Quality) - 18 codes
O1.01 Works/Doesn't Work: Basic functionality success or failure
O1.02 Performance Level: How well it operates
O1.03 Durability: Longevity and resistance to wear
O1.04 Reliability: Consistency of function over time
O1.05 Outcome Achievement: Did customer accomplish their goal?
O2.01 Materials/Inputs: Quality of components or ingredients
O2.02 Craftsmanship: Skill of construction or execution
O2.03 Presentation: Visual and aesthetic quality
O2.04 Attention to Detail: Finishing touches and refinement
O2.05 Condition at Delivery: State when received
O3.01 All Components Present: Nothing missing from what was promised
O3.02 Feature Availability: Promised features actually work
O3.03 Scope Delivery: Full scope of work completed
O3.04 Documentation: Supporting materials provided
O4.01 Specification Match: Matches what was ordered
O4.02 Personalization: Adapted to individual preferences
O4.03 Flexibility: Can be modified or adjusted
O4.04 Appropriateness: Right solution for the need

### P - PEOPLE (Staff Interactions) - 20 codes
P1.01 Warmth: Friendly and welcoming manner
P1.02 Respect: Treated with dignity
P1.03 Patience: Calm and tolerant approach
P1.04 Enthusiasm: Energy and engagement
P1.05 Empathy: Understanding feelings
P2.01 Knowledge: Expertise and understanding
P2.02 Skill: Technical ability
P2.03 Problem Solving: Ability to find solutions
P2.04 Advice Quality: Helpful recommendations
P2.05 Training Level: Staff training evident
P3.01 Attentiveness: Being present and engaged
P3.02 Initiative: Proactive help
P3.03 Follow-through: Completing promised actions
P3.04 Availability: Being available when needed
P3.05 Dedication: Commitment to helping
P4.01 Clarity: Clear communication
P4.02 Listening: Understanding customer needs
P4.03 Transparency: Honest and open
P4.04 Honesty: Truthful communication
P4.05 Proactive Updates: Keeping customer informed

### J - JOURNEY (Process & Timing) - 20 codes
J1.01 Speed: How fast things happen
J1.02 Punctuality: On-time delivery
J1.03 Queue Management: Handling of waiting customers
J1.04 Punctuality: Meeting scheduled times
J1.05 Pacing: Appropriate speed (not rushed/dragged)
J2.01 Simplicity: Easy process
J2.02 Friction: Obstacles encountered
J2.03 Navigation: Finding what you need
J2.04 Booking Availability: Slots/capacity when needed
J2.05 Inventory: Stock availability
J3.01 Consistency: Same experience every time
J3.02 Accuracy: Getting it right
J3.03 Uptime: System availability
J3.04 Data Accuracy: Correct info in systems
J3.05 Integration: Systems work together
J4.01 Problem Recognition: Acknowledging issues
J4.02 Resolution Speed: How fast problems get fixed
J4.03 Resolution Fairness: Fair handling of issues
J4.04 Escalation: Getting to right person
J4.05 Closure: Issue fully resolved

### E - ENVIRONMENT (Physical & Digital Space) - 20 codes
E1.01 Cleanliness: How clean the space is
E1.02 Comfort: Physical comfort
E1.03 Space Design: Layout and organization
E1.04 Ambiance: Atmosphere and vibe
E1.05 Comfort: Physical comfort
E2.01 Lighting: Light quality and level
E2.02 Sound/Noise: Audio environment
E2.03 Temperature: Climate control
E2.04 Visual Design: Aesthetics of interface
E2.05 Mobile Experience: Mobile usability
E3.01 Interface Design: Digital UX/UI
E3.02 App/Website Speed: Digital performance
E3.03 Usability: Ease of digital use
E3.04 Health Safety: Health precautions
E3.05 Cyber Security: Digital security
E4.01 Safety: Physical safety
E4.02 Security: Protection of belongings/data
E4.03 Health/Hygiene: Health standards
E4.04 Social Responsibility: Ethical practices
E4.05 Community Impact: Local community effect

### A - ACCESS (Availability & Accessibility) - 20 codes
A1.01 Hours: Operating hours
A1.02 Booking Availability: Appointment slots
A1.03 Inventory: Product availability
A1.04 Wayfinding: Finding destination
A1.05 Physical Accessibility: Disability accommodations
A2.01 Physical Access: Mobility accessibility
A2.02 Language Access: Language accommodation
A2.03 Digital Accessibility: Screen reader/a11y
A2.04 Language Accessibility: Multilingual support
A2.05 Hours of Operation: Service availability times
A3.01 Diversity Welcome: All backgrounds welcome
A3.02 Accommodation: Special needs accommodation
A3.03 Response Time: Speed of getting answers
A3.04 Documentation Clarity: Clear instructions
A3.05 Support Accessibility: Getting help when needed
A4.01 Location: Physical location convenience
A4.02 Parking: Parking availability
A4.03 Multiple Channels: Ways to engage
A4.04 Payment Flexibility: Multiple payment options
A4.05 Refund Accessibility: Getting money back

### V - VALUE (Pricing & Costs) - 20 codes ⚠️ USE FOR ALL PRICE/COST/FEE MENTIONS
V1.01 Price Level: Cost amount ("cheap", "expensive", "affordable", "€", "$")
V1.02 Price Fairness: Fair for what you get
V1.03 Hidden Costs: Unexpected charges, surprise fees, hidden fees, extra charges
V1.04 Price Transparency: Clear pricing upfront
V1.05 Price Stability: Consistent pricing
V2.01 Clear Pricing: Easy to understand costs
V2.02 Honest Billing: Accurate charges
V2.03 Policy Clarity: Clear terms and conditions
V2.04 Quality-Price Ratio: Worth vs cost
V2.05 Competitive Value: Compared to alternatives
V3.01 Time Investment: Time required
V3.02 Hassle Factor: Difficulty and inconvenience
V3.03 Mental Load: Cognitive effort required
V3.04 Promotion Clarity: Clear offer terms
V3.05 Reward Redemption: Using points/rewards
V4.01 Value for Money: Worth what you paid
V4.02 ROI: Return on investment
V4.03 Overall Satisfaction: Happy with the exchange
V4.04 Billing Accuracy: Correct charges
V4.05 Billing Resolution: Fixing billing issues

### R - RELATIONSHIP (Trust & Loyalty) - 20 codes
R1.01 Honesty: Truthfulness
R1.02 Ethics: Ethical behavior, deceptive practices, scams
R1.03 Promises Kept: Following through on promises
R1.04 Ethics: Ethical behavior
R1.05 Accountability: Taking responsibility
R2.01 Consistency: Reliable over time
R2.02 Trustworthiness: Can be trusted
R2.03 Accountability: Takes responsibility
R2.04 Predictability: Consistent experience
R2.05 Standards: Meeting quality standards
R3.01 Error Acknowledgment: Admits mistakes
R3.02 Apology Quality: Sincere apologies
R3.03 Making It Right: Correcting mistakes
R3.04 Personal Connection: Human touch
R3.05 Going Extra Mile: Beyond expectations
R4.01 Customer Recognition: Remembers customers
R4.02 Loyalty Rewards: Rewards for loyalty
R4.03 Long-term Relationship: Builds relationships
R4.04 Service Recovery: Making things right
R4.05 Feedback Response: Acting on feedback

## CLASSIFICATION EXAMPLES (Critical Distinctions)

**PRICING/COSTS → V codes (Value), NOT P codes:**
- "Cheap prices", "good price", "€50" → V1.01 Price Level
- "Hidden charges", "surprise fees", "extra €35" → V1.03 Hidden Costs
- "Great value for money" → V4.01 Value for Money
- "Overcharged", "wrong amount" → V4.04 Billing Accuracy

**STAFF BEHAVIOR → P codes (People):**
- "Staff was friendly", "welcoming" → P1.01 Warmth
- "Rude", "disrespectful", "ignored us" → P1.02 Respect
- "Patient", "took their time" → P1.03 Patience
- "Knowledgeable", "expert" → P2.01 Knowledge

**DECEPTION/ETHICS → R codes (Relationship):**
- "They lied", "misleading" → R1.01 Honesty
- "Felt scammed", "dishonest practices" → R1.02 Ethics
- "Didn't honor the deal" → R1.03 Promises Kept

## 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

## 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
  }
}"""


class LLMClientBase(ABC):
    """Abstract base class for LLM clients."""

    def __init__(self, config: Config):
        self.config = config
        self.total_tokens_used = 0
        self.total_cost_usd = 0.0
        self._custom_prompt: str | None = None

    def set_prompt(self, prompt: str) -> None:
        """
        Set a custom system prompt (e.g., built dynamically from database).

        Args:
            prompt: The system prompt to use for classification
        """
        self._custom_prompt = prompt

    def get_prompt(self) -> str:
        """Get the current system prompt (custom or default)."""
        return self._custom_prompt or SYSTEM_PROMPT

    @abstractmethod
    async def classify(
        self,
        review_text: str,
        profile: str = "standard",
    ) -> tuple[LLMClassificationResponse, dict[str, Any]]:
        """
        Classify a review and extract spans.

        Args:
            review_text: The review text to classify
            profile: Classification profile (lite/core/standard/full)

        Returns:
            Tuple of (classification response, metadata dict with tokens/cost)
        """
        pass

    @abstractmethod
    async def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 0.7,
        max_tokens: int = 4000,
    ) -> str:
        """
        Generate text using the LLM (for synthesis, narratives, etc.).

        Args:
            system_prompt: System instructions
            user_prompt: User content/context
            temperature: Creativity level (0-1)
            max_tokens: Maximum response length

        Returns:
            Generated text response
        """
        pass

    @abstractmethod
    async def close(self) -> None:
        """Close the client and cleanup resources."""
        pass


class OpenAIClient(LLMClientBase):
    """OpenAI LLM client implementation."""

    # Pricing per 1M tokens (as of 2024)
    PRICING = {
        "gpt-4o": {"input": 5.0, "output": 15.0},
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "gpt-4-turbo": {"input": 10.0, "output": 30.0},
        "gpt-3.5-turbo": {"input": 0.50, "output": 1.50},
    }

    def __init__(self, config: Config):
        super().__init__(config)
        from openai import AsyncOpenAI

        self.client = AsyncOpenAI(api_key=config.get_llm_api_key())
        self.model = config.llm_model

    async def classify(
        self,
        review_text: str,
        profile: str = "standard",
    ) -> tuple[LLMClassificationResponse, dict[str, Any]]:
        """Classify using OpenAI."""
        start_time = time.time()

        messages = [
            {"role": "system", "content": self.get_prompt()},
            {
                "role": "user",
                "content": f'Classify this review:\n\n"{review_text}"',
            },
        ]

        response = await self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=self.config.llm_temperature,
            response_format={"type": "json_object"},
            timeout=self.config.llm_timeout_seconds,
        )

        # Parse response
        content = response.choices[0].message.content
        if not content:
            raise ValueError("Empty response from OpenAI")

        result = json.loads(content)

        # Calculate costs
        input_tokens = response.usage.prompt_tokens if response.usage else 0
        output_tokens = response.usage.completion_tokens if response.usage else 0
        total_tokens = input_tokens + output_tokens

        pricing = self.PRICING.get(self.model, {"input": 0.15, "output": 0.60})
        cost = (input_tokens * pricing["input"] + output_tokens * pricing["output"]) / 1_000_000

        self.total_tokens_used += total_tokens
        self.total_cost_usd += cost

        metadata = {
            "model": self.model,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "total_tokens": total_tokens,
            "cost_usd": cost,
            "latency_ms": int((time.time() - start_time) * 1000),
        }

        return result, metadata

    async def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 0.7,
        max_tokens: int = 4000,
    ) -> str:
        """Generate text using OpenAI."""
        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ]

        response = await self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens,
            response_format={"type": "json_object"},
            timeout=self.config.llm_timeout_seconds,
        )

        content = response.choices[0].message.content
        if not content:
            raise ValueError("Empty response from OpenAI")

        # Track usage
        if response.usage:
            input_tokens = response.usage.prompt_tokens
            output_tokens = response.usage.completion_tokens
            pricing = self.PRICING.get(self.model, {"input": 0.15, "output": 0.60})
            cost = (input_tokens * pricing["input"] + output_tokens * pricing["output"]) / 1_000_000
            self.total_tokens_used += input_tokens + output_tokens
            self.total_cost_usd += cost

        return content

    async def close(self) -> None:
        """Close the OpenAI client."""
        await self.client.close()


class AnthropicClient(LLMClientBase):
    """Anthropic LLM client implementation."""

    # Pricing per 1M tokens (as of 2024)
    PRICING = {
        "claude-3-opus-20240229": {"input": 15.0, "output": 75.0},
        "claude-3-sonnet-20240229": {"input": 3.0, "output": 15.0},
        "claude-3-haiku-20240307": {"input": 0.25, "output": 1.25},
        "claude-3-5-sonnet-20241022": {"input": 3.0, "output": 15.0},
    }

    def __init__(self, config: Config):
        super().__init__(config)
        from anthropic import AsyncAnthropic

        self.client = AsyncAnthropic(api_key=config.get_llm_api_key())
        self.model = config.llm_model

    async def classify(
        self,
        review_text: str,
        profile: str = "standard",
    ) -> tuple[LLMClassificationResponse, dict[str, Any]]:
        """Classify using Anthropic."""
        start_time = time.time()

        response = await self.client.messages.create(
            model=self.model,
            max_tokens=4096,
            system=self.get_prompt(),
            messages=[
                {
                    "role": "user",
                    "content": f'Classify this review and return JSON only:\n\n"{review_text}"',
                },
            ],
            temperature=self.config.llm_temperature,
        )

        # Parse response
        content = response.content[0].text if response.content else ""
        if not content:
            raise ValueError("Empty response from Anthropic")

        # Try to extract JSON from response
        result = self._extract_json(content)

        # Calculate costs
        input_tokens = response.usage.input_tokens
        output_tokens = response.usage.output_tokens
        total_tokens = input_tokens + output_tokens

        pricing = self.PRICING.get(self.model, {"input": 3.0, "output": 15.0})
        cost = (input_tokens * pricing["input"] + output_tokens * pricing["output"]) / 1_000_000

        self.total_tokens_used += total_tokens
        self.total_cost_usd += cost

        metadata = {
            "model": self.model,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "total_tokens": total_tokens,
            "cost_usd": cost,
            "latency_ms": int((time.time() - start_time) * 1000),
        }

        return result, metadata

    async def generate(
        self,
        system_prompt: str,
        user_prompt: str,
        temperature: float = 0.7,
        max_tokens: int = 4000,
    ) -> str:
        """Generate text using Anthropic."""
        response = await self.client.messages.create(
            model=self.model,
            max_tokens=max_tokens,
            system=system_prompt,
            messages=[{"role": "user", "content": user_prompt}],
            temperature=temperature,
        )

        content = response.content[0].text if response.content else ""
        if not content:
            raise ValueError("Empty response from Anthropic")

        # Track usage
        input_tokens = response.usage.input_tokens
        output_tokens = response.usage.output_tokens
        pricing = self.PRICING.get(self.model, {"input": 3.0, "output": 15.0})
        cost = (input_tokens * pricing["input"] + output_tokens * pricing["output"]) / 1_000_000
        self.total_tokens_used += input_tokens + output_tokens
        self.total_cost_usd += cost

        # Extract JSON from response (handles code blocks)
        return self._extract_json_string(content)

    def _extract_json_string(self, content: str) -> str:
        """Extract JSON string from response, handling markdown code blocks."""
        import re
        content = content.strip()

        # If it starts with {, return as-is
        if content.startswith("{"):
            return content

        # Try to find JSON in code blocks
        json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", content)
        if json_match:
            return json_match.group(1)

        # Try to find JSON object
        json_match = re.search(r"\{[\s\S]*\}", content)
        if json_match:
            return json_match.group(0)

        return content

    def _extract_json(self, content: str) -> dict[str, Any]:
        """Extract JSON from response, handling markdown code blocks."""
        content = content.strip()

        # Try direct parse first
        try:
            return json.loads(content)
        except json.JSONDecodeError:
            pass

        # Try to find JSON in code blocks
        import re

        json_match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", content)
        if json_match:
            return json.loads(json_match.group(1))

        # Try to find JSON object
        json_match = re.search(r"\{[\s\S]*\}", content)
        if json_match:
            return json.loads(json_match.group(0))

        raise ValueError(f"Could not extract JSON from response: {content[:200]}")

    async def close(self) -> None:
        """Close the Anthropic client."""
        await self.client.close()


class LLMClient:
    """
    Factory class for LLM clients.

    Usage:
        client = LLMClient.create(config)
        result, metadata = await client.classify(review_text)
        await client.close()
    """

    @staticmethod
    def create(config: Config) -> LLMClientBase:
        """
        Create an LLM client based on configuration.

        Args:
            config: Pipeline configuration

        Returns:
            LLM client instance (OpenAI or Anthropic)
        """
        if config.llm_provider == "openai":
            return OpenAIClient(config)
        elif config.llm_provider == "anthropic":
            return AnthropicClient(config)
        else:
            raise ValueError(f"Unsupported LLM provider: {config.llm_provider}")


def create_fallback_response(review_text: str) -> LLMClassificationResponse:
    """
    Create a fallback classification response when LLM fails.

    Args:
        review_text: Original review text

    Returns:
        Minimal valid classification response
    """
    return {
        "spans": [
            {
                "span_index": 0,
                "span_text": review_text,
                "span_start": 0,
                "span_end": len(review_text),
                "urt_primary": "O1.01",
                "urt_secondary": [],
                "valence": "V0",
                "intensity": "I1",
                "specificity": "S1",
                "actionability": "A1",
                "temporal": "TC",
                "evidence": "ES",
                "comparative": "CR-N",
                "is_primary": True,
                "confidence": "low",
                "entity": None,
                "entity_type": None,
                "relation_type": None,
                "related_span_index": None,
                "usn": "URT:S:O1.01:01:11TC.ES.N",
            }
        ],
        "review_summary": {
            "dominant_valence": "V0",
            "dominant_domain": "O",
            "span_count": 1,
            "has_comparative": False,
            "has_entity": False,
        },
    }
