"""
Stage 4: Fact Aggregation

Pre-aggregate span/review data into fact_timeseries for fast dashboard queries.

Responsibilities:
- Aggregate spans by URT code per time bucket
- Calculate valence/intensity distributions
- Compute strength scores (trust-weighted)
- UPSERT into fact_timeseries table
"""

from __future__ import annotations

import logging
from collections import defaultdict
from datetime import date, datetime, timedelta
from typing import TYPE_CHECKING, Any

from reviewiq_pipeline.contracts import (
    FactRecord,
    Stage4Input,
    Stage4Output,
    Stage4Stats,
)

if TYPE_CHECKING:
    from reviewiq_pipeline.config import Config
    from reviewiq_pipeline.db.connection import DatabasePool
    from reviewiq_pipeline.db.repositories import FactRepository

logger = logging.getLogger(__name__)


class Stage4Aggregator:
    """
    Stage 4: Aggregate span data into time series facts.

    This stage:
    1. Queries span data for a business/date range
    2. Aggregates by URT code and time bucket
    3. Calculates valence/intensity distributions
    4. Computes trust-weighted strength scores
    5. UPSERTs results into fact_timeseries table
    """

    def __init__(
        self,
        config: Config,
        db: DatabasePool | None = None,
        fact_repo: FactRepository | None = None,
    ):
        self.config = config
        self.db = db
        self.fact_repo = fact_repo

    async def process(self, input_data: Stage4Input) -> Stage4Output:
        """
        Process aggregation for a business and date.

        Args:
            input_data: Stage 4 input with aggregation parameters

        Returns:
            Stage4Output with aggregated facts and stats
        """
        logger.info(
            f"Stage 4: Aggregating for business {input_data['business_id']} "
            f"on {input_data['date']}"
        )

        facts_written: list[FactRecord] = []
        locations_processed = 0
        codes_aggregated = set()

        # Get date range based on bucket types
        target_date = datetime.strptime(input_data["date"], "%Y-%m-%d").date()

        for bucket_type in input_data["bucket_types"]:
            start_date, end_date = self._get_bucket_range(target_date, bucket_type)
            period_date = self._get_period_date(target_date, bucket_type)

            # Get aggregation data from database
            if self.fact_repo:
                span_data = await self.fact_repo.get_aggregation_data(
                    input_data["business_id"],
                    start_date,
                    end_date,
                )
                place_ids = await self.fact_repo.get_place_ids_for_business(
                    input_data["business_id"]
                )
            else:
                span_data = []
                place_ids = []

            # Aggregate by place_id and URT code
            for place_id in place_ids + ["ALL"]:
                place_data = [
                    s for s in span_data
                    if place_id == "ALL" or s["place_id"] == place_id
                ]

                if not place_data:
                    continue

                locations_processed += 1 if place_id != "ALL" else 0

                # Aggregate by URT code
                code_facts = self._aggregate_by_code(
                    place_data,
                    input_data["business_id"],
                    place_id,
                    period_date,
                    bucket_type,
                    input_data["taxonomy_version"],
                )

                for fact in code_facts:
                    facts_written.append(fact)
                    codes_aggregated.add(fact["subject_id"])

                    if self.fact_repo:
                        await self.fact_repo.upsert_fact(fact)

                # Aggregate by domain
                domain_facts = self._aggregate_by_domain(
                    place_data,
                    input_data["business_id"],
                    place_id,
                    period_date,
                    bucket_type,
                    input_data["taxonomy_version"],
                )

                for fact in domain_facts:
                    facts_written.append(fact)

                    if self.fact_repo:
                        await self.fact_repo.upsert_fact(fact)

                # Overall aggregation
                overall_fact = self._aggregate_overall(
                    place_data,
                    input_data["business_id"],
                    place_id,
                    period_date,
                    bucket_type,
                    input_data["taxonomy_version"],
                )

                facts_written.append(overall_fact)

                if self.fact_repo:
                    await self.fact_repo.upsert_fact(overall_fact)

        logger.info(
            f"Stage 4 complete: {len(facts_written)} facts written, "
            f"{len(codes_aggregated)} unique codes"
        )

        return Stage4Output(
            facts_written=facts_written,
            stats=Stage4Stats(
                business_id=input_data["business_id"],
                date=input_data["date"],
                locations_processed=locations_processed,
                codes_aggregated=len(codes_aggregated),
                facts_upserted=len(facts_written),
            ),
        )

    def _get_bucket_range(
        self,
        target_date: date,
        bucket_type: str,
    ) -> tuple[date, date]:
        """Get start and end dates for a time bucket."""
        if bucket_type == "day":
            return target_date, target_date
        elif bucket_type == "week":
            # Week starts on Monday
            start = target_date - timedelta(days=target_date.weekday())
            end = start + timedelta(days=6)
            return start, end
        elif bucket_type == "month":
            start = target_date.replace(day=1)
            # Get last day of month
            if target_date.month == 12:
                end = target_date.replace(year=target_date.year + 1, month=1, day=1) - timedelta(days=1)
            else:
                end = target_date.replace(month=target_date.month + 1, day=1) - timedelta(days=1)
            return start, end
        else:
            raise ValueError(f"Unknown bucket type: {bucket_type}")

    def _get_period_date(self, target_date: date, bucket_type: str) -> date:
        """Get the period date for a bucket."""
        if bucket_type == "day":
            return target_date
        elif bucket_type == "week":
            # Week starts on Monday
            return target_date - timedelta(days=target_date.weekday())
        elif bucket_type == "month":
            return target_date.replace(day=1)
        else:
            return target_date

    def _aggregate_by_code(
        self,
        span_data: list[dict[str, Any]],
        business_id: str,
        place_id: str,
        period_date: date,
        bucket_type: str,
        taxonomy_version: str,
    ) -> list[FactRecord]:
        """Aggregate spans by URT code."""
        code_groups: dict[str, list[dict]] = defaultdict(list)

        for span in span_data:
            code_groups[span["urt_primary"]].append(span)

        facts = []
        for code, spans in code_groups.items():
            fact = self._compute_fact_metrics(
                spans,
                business_id,
                place_id,
                period_date,
                bucket_type,
                "urt_code",
                code,
                taxonomy_version,
            )
            facts.append(fact)

        return facts

    def _aggregate_by_domain(
        self,
        span_data: list[dict[str, Any]],
        business_id: str,
        place_id: str,
        period_date: date,
        bucket_type: str,
        taxonomy_version: str,
    ) -> list[FactRecord]:
        """Aggregate spans by domain (first letter of URT code)."""
        domain_groups: dict[str, list[dict]] = defaultdict(list)

        for span in span_data:
            domain = span["urt_primary"][0]  # First letter
            domain_groups[domain].append(span)

        facts = []
        for domain, spans in domain_groups.items():
            fact = self._compute_fact_metrics(
                spans,
                business_id,
                place_id,
                period_date,
                bucket_type,
                "domain",
                domain,
                taxonomy_version,
            )
            facts.append(fact)

        return facts

    def _aggregate_overall(
        self,
        span_data: list[dict[str, Any]],
        business_id: str,
        place_id: str,
        period_date: date,
        bucket_type: str,
        taxonomy_version: str,
    ) -> FactRecord:
        """Aggregate all spans for overall metrics."""
        return self._compute_fact_metrics(
            span_data,
            business_id,
            place_id,
            period_date,
            bucket_type,
            "overall",
            "all",
            taxonomy_version,
        )

    def _compute_fact_metrics(
        self,
        spans: list[dict[str, Any]],
        business_id: str,
        place_id: str,
        period_date: date,
        bucket_type: str,
        subject_type: str,
        subject_id: str,
        taxonomy_version: str,
    ) -> FactRecord:
        """
        Compute aggregated metrics for a group of spans.

        Args:
            spans: List of span data
            business_id: Business identifier
            place_id: Place ID or 'ALL'
            period_date: Period date string
            bucket_type: day/week/month
            subject_type: overall/urt_code/domain/issue
            subject_id: Subject identifier
            taxonomy_version: Taxonomy version

        Returns:
            FactRecord with computed metrics
        """
        if not spans:
            return self._empty_fact(
                business_id, place_id, period_date, bucket_type,
                subject_type, subject_id, taxonomy_version,
            )

        # Count unique reviews
        review_ids = set()
        for span in spans:
            # Assuming span has review_id in metadata
            review_id = span.get("review_id", span.get("span_id", ""))
            review_ids.add(review_id)

        span_count = len(spans)
        review_count = len(review_ids) if review_ids else span_count

        # Valence counts
        negative_count = sum(1 for s in spans if s["valence"] == "V-")
        positive_count = sum(1 for s in spans if s["valence"] == "V+")
        neutral_count = sum(1 for s in spans if s["valence"] == "V0")
        mixed_count = sum(1 for s in spans if s["valence"] == "V±")

        # Intensity counts
        i1_count = sum(1 for s in spans if s["intensity"] == "I1")
        i2_count = sum(1 for s in spans if s["intensity"] == "I2")
        i3_count = sum(1 for s in spans if s["intensity"] == "I3")

        # Comparative counts
        cr_better = sum(1 for s in spans if s.get("comparative") == "CR-B")
        cr_worse = sum(1 for s in spans if s.get("comparative") == "CR-W")
        cr_same = sum(1 for s in spans if s.get("comparative") == "CR-S")

        # Calculate strength scores
        strength_score = self._compute_strength_score(spans)
        negative_strength = self._compute_strength_score(
            [s for s in spans if s["valence"] in ("V-", "V±")]
        )
        positive_strength = self._compute_strength_score(
            [s for s in spans if s["valence"] == "V+"]
        )

        # Trust-weighted scores
        trust_weighted_strength = self._compute_trust_weighted_strength(spans)
        trust_weighted_negative = self._compute_trust_weighted_strength(
            [s for s in spans if s["valence"] in ("V-", "V±")]
        )

        # Average rating
        ratings = [s["rating"] for s in spans if s.get("rating")]
        avg_rating = sum(ratings) / len(ratings) if ratings else None

        return FactRecord(
            business_id=business_id,
            place_id=place_id,
            period_date=period_date,
            bucket_type=bucket_type,
            subject_type=subject_type,  # type: ignore
            subject_id=subject_id,
            taxonomy_version=taxonomy_version,
            review_count=review_count,
            span_count=span_count,
            negative_count=negative_count,
            positive_count=positive_count,
            neutral_count=neutral_count,
            mixed_count=mixed_count,
            strength_score=strength_score,
            negative_strength=negative_strength,
            positive_strength=positive_strength,
            avg_rating=avg_rating,
            i1_count=i1_count,
            i2_count=i2_count,
            i3_count=i3_count,
            cr_better=cr_better,
            cr_worse=cr_worse,
            cr_same=cr_same,
            trust_weighted_strength=trust_weighted_strength,
            trust_weighted_negative=trust_weighted_negative,
        )

    def _compute_strength_score(self, spans: list[dict[str, Any]]) -> float:
        """
        Compute strength score from intensity distribution.

        Score: sum of (intensity_weight * valence_multiplier)
        I1=1, I2=2, I3=4 (exponential)
        V-=1, V±=0.5, V0=0, V+=1
        """
        if not spans:
            return 0.0

        intensity_weights = {"I1": 1, "I2": 2, "I3": 4}
        valence_multipliers = {"V-": 1.0, "V±": 0.5, "V0": 0.0, "V+": 1.0}

        total = 0.0
        for span in spans:
            intensity = span.get("intensity", "I1")
            valence = span.get("valence", "V0")
            weight = intensity_weights.get(intensity, 1)
            multiplier = valence_multipliers.get(valence, 0)
            total += weight * multiplier

        return total

    def _compute_trust_weighted_strength(self, spans: list[dict[str, Any]]) -> float:
        """
        Compute trust-weighted strength score.

        Similar to strength score but weighted by trust_score.
        """
        if not spans:
            return 0.0

        intensity_weights = {"I1": 1, "I2": 2, "I3": 4}
        valence_multipliers = {"V-": 1.0, "V±": 0.5, "V0": 0.0, "V+": 1.0}

        total = 0.0
        for span in spans:
            intensity = span.get("intensity", "I1")
            valence = span.get("valence", "V0")
            trust = span.get("trust_score", 0.5)

            weight = intensity_weights.get(intensity, 1)
            multiplier = valence_multipliers.get(valence, 0)
            total += weight * multiplier * trust

        return total

    def _empty_fact(
        self,
        business_id: str,
        place_id: str,
        period_date: date,
        bucket_type: str,
        subject_type: str,
        subject_id: str,
        taxonomy_version: str,
    ) -> FactRecord:
        """Create an empty fact record with zero counts."""
        return FactRecord(
            business_id=business_id,
            place_id=place_id,
            period_date=period_date,
            bucket_type=bucket_type,
            subject_type=subject_type,  # type: ignore
            subject_id=subject_id,
            taxonomy_version=taxonomy_version,
            review_count=0,
            span_count=0,
            negative_count=0,
            positive_count=0,
            neutral_count=0,
            mixed_count=0,
            strength_score=0.0,
            negative_strength=0.0,
            positive_strength=0.0,
            avg_rating=None,
            i1_count=0,
            i2_count=0,
            i3_count=0,
            cr_better=0,
            cr_worse=0,
            cr_same=0,
            trust_weighted_strength=0.0,
            trust_weighted_negative=0.0,
        )
