"""Data access layer for pipeline operations.

All tables live in the 'pipeline' schema, keeping them decoupled from the
main scraper schema while sharing the same database.
"""

from __future__ import annotations

import json
import logging
from datetime import date, datetime
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from reviewiq_pipeline.contracts import (
        ClassifiedReview,
        ExtractedSpan,
        FactRecord,
        NormalizedReview,
        RawReview,
        RoutedSpan,
    )
    from reviewiq_pipeline.db.connection import DatabasePool

logger = logging.getLogger(__name__)

# Schema prefix for all pipeline tables
SCHEMA = "pipeline"


class ReviewRepository:
    """Repository for review data operations."""

    def __init__(self, db: DatabasePool):
        self.db = db

    async def insert_raw_review(
        self,
        review: RawReview,
        place_id: str,
        source: str = "google",
    ) -> int:
        """Insert a raw review and return its ID."""
        query = """
            INSERT INTO pipeline.reviews_raw (
                source, review_id, place_id, raw_payload,
                review_text, rating, review_time, reviewer_name, reviewer_id,
                review_version, pulled_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW())
            ON CONFLICT (source, review_id, review_version) DO UPDATE SET
                pulled_at = NOW()
            RETURNING id
        """
        raw_id = await self.db.fetchval(
            query,
            source,
            review["review_id"],
            place_id,
            json.dumps(review.get("raw_payload", {})),
            review.get("text"),
            review["rating"],
            review["review_time"],
            review["author_name"],
            review.get("author_id"),
            1,  # Initial version
        )
        return raw_id

    async def insert_enriched_review(
        self,
        review: NormalizedReview,
        raw_id: int,
        job_id: str | None = None,
    ) -> int:
        """Insert an enriched review stub (pre-classification)."""
        query = """
            INSERT INTO pipeline.reviews_enriched (
                source, review_id, review_version, is_latest, raw_id,
                business_id, place_id, text, text_normalized, rating, review_time,
                language, taxonomy_version, job_id
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::uuid)
            ON CONFLICT (source, review_id, review_version) DO UPDATE SET
                is_latest = EXCLUDED.is_latest,
                job_id = COALESCE(EXCLUDED.job_id, pipeline.reviews_enriched.job_id)
            RETURNING id
        """
        enriched_id = await self.db.fetchval(
            query,
            review["source"],
            review["review_id"],
            review["review_version"],
            True,  # is_latest
            raw_id,
            review["business_id"],
            review["place_id"],
            review["text"],
            review["text_normalized"],
            review["rating"],
            review["review_time"],
            review["text_language"],
            "v5.1",  # taxonomy_version - will be updated by Stage 2
            job_id,
        )
        return enriched_id

    async def update_enriched_with_classification(
        self,
        classified: ClassifiedReview,
        model_version: str,
        taxonomy_version: str,
    ) -> None:
        """Update an enriched review with classification results."""
        query = """
            UPDATE pipeline.reviews_enriched SET
                urt_primary = $1,
                urt_secondary = $2,
                valence = $3,
                intensity = $4,
                comparative = $5,
                staff_mentions = $6,
                quotes = $7,
                embedding = $8,
                trust_score = $9,
                classification_model = $10,
                classification_confidence = $11,
                taxonomy_version = $12,
                processed_at = NOW()
            WHERE source = $13
              AND review_id = $14
              AND review_version = $15
        """
        await self.db.execute(
            query,
            classified["urt_primary"],
            classified.get("urt_secondary", []),
            classified["valence"],
            classified["intensity"],
            classified.get("comparative", "CR-N"),
            classified.get("staff_mentions", []),
            json.dumps(classified.get("quotes", {})),
            classified.get("embedding", []),
            classified.get("trust_score", 0.5),
            model_version,
            json.dumps(classified.get("classification_confidence", {})),
            taxonomy_version,
            classified["source"],
            classified["review_id"],
            classified["review_version"],
        )

    async def get_unclassified_reviews(
        self,
        limit: int = 100,
    ) -> list[dict[str, Any]]:
        """Get reviews that haven't been classified yet."""
        query = """
            SELECT
                source, review_id, review_version, business_id, place_id,
                text, text_normalized, rating, review_time
            FROM pipeline.reviews_enriched
            WHERE urt_primary IS NULL
              AND is_latest = TRUE
            ORDER BY review_time DESC
            LIMIT $1
        """
        rows = await self.db.fetch(query, limit)
        return [dict(r) for r in rows]

    async def get_review_by_id(
        self,
        source: str,
        review_id: str,
        review_version: int,
    ) -> dict[str, Any] | None:
        """Get a specific review by its composite key."""
        query = """
            SELECT * FROM pipeline.reviews_enriched
            WHERE source = $1 AND review_id = $2 AND review_version = $3
        """
        row = await self.db.fetchrow(query, source, review_id, review_version)
        return dict(row) if row else None

    async def check_duplicate(
        self,
        content_hash: str,
        business_id: str,
    ) -> str | None:
        """Check if a content hash already exists, return dedup_group_id if so."""
        # For now, we check by querying the first occurrence
        # A proper dedup table would be better for production
        query = """
            SELECT review_id FROM pipeline.reviews_enriched
            WHERE business_id = $1
              AND text_normalized IS NOT NULL
            LIMIT 1
        """
        # Simplified - in production, use a separate dedup table with content_hash index
        return None


class SpanRepository:
    """Repository for span data operations."""

    def __init__(self, db: DatabasePool):
        self.db = db

    async def insert_span(
        self,
        span: ExtractedSpan,
        business_id: str,
        place_id: str,
        source: str,
        review_id: str,
        review_version: int,
        review_time: str,
        batch_id: str,
        model_version: str,
        taxonomy_version: str,
        job_id: str | None = None,
    ) -> None:
        """Insert a span into the database."""
        query = """
            INSERT INTO pipeline.review_spans (
                span_id, business_id, place_id, source, review_id, review_version,
                span_index, span_text, span_start, span_end,
                profile, urt_primary, urt_secondary, valence, intensity, comparative,
                specificity, actionability, temporal, evidence,
                entity, entity_type, entity_normalized,
                relation_type, related_span_id, causal_chain,
                is_primary, is_active, review_time,
                confidence, usn, taxonomy_version, model_version, ingest_batch_id,
                job_id
            ) VALUES (
                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
                $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
                $21, $22, $23, $24, $25, $26, $27, $28, $29, $30,
                $31, $32, $33, $34, $35::uuid
            )
            ON CONFLICT (span_id) DO UPDATE SET
                is_active = EXCLUDED.is_active,
                job_id = COALESCE(EXCLUDED.job_id, pipeline.review_spans.job_id)
        """
        # Build related_span_id from index if needed
        related_span_id = None
        if span.get("related_span_index") is not None:
            # Build the related span_id (would need the actual span_id mapping)
            pass

        await self.db.execute(
            query,
            span["span_id"],
            business_id,
            place_id,
            source,
            review_id,
            review_version,
            span["span_index"],
            span["span_text"],
            span["span_start"],
            span["span_end"],
            span.get("profile", "standard"),
            span["urt_primary"],
            span.get("urt_secondary", []),
            span["valence"],
            span["intensity"],
            span.get("comparative", "CR-N"),
            span.get("specificity"),
            span.get("actionability"),
            span.get("temporal"),
            span.get("evidence"),
            span.get("entity"),
            span.get("entity_type"),
            span.get("entity_normalized"),
            span.get("relation_type"),
            related_span_id,
            json.dumps(span.get("causal_chain")) if span.get("causal_chain") else None,
            span.get("is_primary", False),
            True,  # is_active
            review_time,
            span.get("confidence", "medium"),
            span["usn"],
            taxonomy_version,
            model_version,
            batch_id,
            job_id,
        )

    async def get_unrouted_negative_spans(
        self,
        limit: int = 100,
    ) -> list[dict[str, Any]]:
        """Get negative spans that haven't been routed to issues yet."""
        query = """
            SELECT
                rs.span_id, rs.business_id, rs.place_id,
                rs.urt_primary, rs.valence, rs.intensity,
                rs.entity_normalized, rs.review_time, rs.confidence,
                re.trust_score
            FROM pipeline.review_spans rs
            JOIN pipeline.reviews_enriched re ON (
                re.source = rs.source
                AND re.review_id = rs.review_id
                AND re.review_version = rs.review_version
            )
            WHERE rs.is_active = TRUE
              AND rs.valence IN ('V-', 'V±')
              AND NOT EXISTS (
                SELECT 1 FROM pipeline.issue_spans iss WHERE iss.span_id = rs.span_id
              )
            ORDER BY rs.review_time DESC
            LIMIT $1
        """
        rows = await self.db.fetch(query, limit)
        return [dict(r) for r in rows]

    async def get_span_by_id(self, span_id: str) -> dict[str, Any] | None:
        """Get a span by its ID."""
        query = "SELECT * FROM pipeline.review_spans WHERE span_id = $1"
        row = await self.db.fetchrow(query, span_id)
        return dict(row) if row else None

    async def deactivate_spans_for_job(self, job_id: str) -> int:
        """Deactivate all spans for a job (used before reclassification).

        Returns the number of spans deactivated.
        """
        result = await self.db.execute(
            """
            UPDATE pipeline.review_spans
            SET is_active = FALSE
            WHERE job_id = $1::uuid AND is_active = TRUE
            """,
            job_id,
        )
        # Extract count from result string like "UPDATE 42"
        if result and result.startswith("UPDATE "):
            return int(result.split()[1])
        return 0


class IssueRepository:
    """Repository for issue data operations."""

    def __init__(self, db: DatabasePool):
        self.db = db

    async def upsert_issue(
        self,
        issue_id: str,
        business_id: str,
        place_id: str,
        primary_subcode: str,
        intensity: str,
        entity: str | None,
        entity_normalized: str | None,
        taxonomy_version: str,
        job_id: str | None = None,
    ) -> bool:
        """Create or update an issue. Returns True if newly created."""
        # First check if exists
        existing = await self.db.fetchval(
            "SELECT 1 FROM pipeline.issues WHERE issue_id = $1",
            issue_id,
        )

        if existing:
            # Update
            await self.db.execute(
                """
                UPDATE pipeline.issues SET
                    span_count = span_count + 1,
                    max_intensity = CASE
                        WHEN $1 = 'I3' THEN 'I3'
                        WHEN $1 = 'I2' AND max_intensity != 'I3' THEN 'I2'
                        ELSE max_intensity
                    END,
                    updated_at = NOW()
                WHERE issue_id = $2
                """,
                intensity,
                issue_id,
            )
            return False
        else:
            # Insert
            domain = primary_subcode[0] if primary_subcode else "O"
            await self.db.execute(
                """
                INSERT INTO pipeline.issues (
                    issue_id, business_id, place_id, primary_subcode, domain,
                    state, priority_score, confidence_score, span_count, max_intensity,
                    entity, entity_normalized, taxonomy_version, job_id
                ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::uuid)
                """,
                issue_id,
                business_id,
                place_id,
                primary_subcode,
                domain,
                "open",
                1.0,  # Initial priority
                1.0,  # Initial confidence
                1,  # Initial span count
                intensity,
                entity,
                entity_normalized,
                taxonomy_version,
                job_id,
            )
            return True

    async def link_span_to_issue(
        self,
        routed: RoutedSpan,
        source: str,
        review_id: str,
        review_version: int,
        intensity: str,
        review_time: str,
        is_primary_match: bool = True,
    ) -> None:
        """Link a span to an issue."""
        await self.db.execute(
            """
            INSERT INTO pipeline.issue_spans (
                issue_id, span_id, source, review_id, review_version,
                is_primary_match, intensity, review_time
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
            ON CONFLICT (span_id) DO NOTHING
            """,
            routed["issue_id"],
            routed["span_id"],
            source,
            review_id,
            review_version,
            is_primary_match,
            intensity,
            review_time,
        )

    async def log_event(
        self,
        issue_id: str,
        event_type: str,
        span_id: str | None = None,
        old_value: str | None = None,
        new_value: str | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> None:
        """Log an issue event for audit trail."""
        await self.db.execute(
            """
            INSERT INTO pipeline.issue_events (
                issue_id, event_type, span_id, old_value, new_value, metadata
            ) VALUES ($1, $2, $3, $4, $5, $6)
            """,
            issue_id,
            event_type,
            span_id,
            old_value,
            new_value,
            json.dumps(metadata) if metadata else None,
        )

    async def get_issue_by_id(self, issue_id: str) -> dict[str, Any] | None:
        """Get an issue by its ID."""
        query = "SELECT * FROM pipeline.issues WHERE issue_id = $1"
        row = await self.db.fetchrow(query, issue_id)
        return dict(row) if row else None

    async def check_span_already_linked(self, span_id: str) -> str | None:
        """Check if a span is already linked to an issue."""
        return await self.db.fetchval(
            "SELECT issue_id FROM pipeline.issue_spans WHERE span_id = $1",
            span_id,
        )

    async def delete_issues_for_job(self, job_id: str) -> int:
        """Delete all issues for a job (used before reclassification).

        Also deletes related issue_spans and issue_events.
        Returns the number of issues deleted.
        """
        # First delete related records
        await self.db.execute(
            """
            DELETE FROM pipeline.issue_spans
            WHERE issue_id IN (
                SELECT issue_id FROM pipeline.issues WHERE job_id = $1::uuid
            )
            """,
            job_id,
        )
        await self.db.execute(
            """
            DELETE FROM pipeline.issue_events
            WHERE issue_id IN (
                SELECT issue_id FROM pipeline.issues WHERE job_id = $1::uuid
            )
            """,
            job_id,
        )
        # Then delete issues
        result = await self.db.execute(
            "DELETE FROM pipeline.issues WHERE job_id = $1::uuid",
            job_id,
        )
        # Extract count from result string like "DELETE 42"
        if result and result.startswith("DELETE "):
            return int(result.split()[1])
        return 0


class FactRepository:
    """Repository for fact time series operations."""

    def __init__(self, db: DatabasePool):
        self.db = db

    async def upsert_fact(self, fact: FactRecord) -> None:
        """Insert or update a fact record."""
        await self.db.execute(
            """
            INSERT INTO pipeline.fact_timeseries (
                business_id, place_id, period_date, bucket_type,
                subject_type, subject_id, taxonomy_version,
                review_count, span_count, negative_count, positive_count,
                neutral_count, mixed_count, strength_score, negative_strength,
                positive_strength, avg_rating, i1_count, i2_count, i3_count,
                cr_better, cr_worse, cr_same,
                trust_weighted_strength, trust_weighted_negative,
                computed_at
            ) VALUES (
                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14,
                $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, NOW()
            )
            ON CONFLICT (
                business_id, place_id, period_date, bucket_type,
                subject_type, subject_id, taxonomy_version
            ) DO UPDATE SET
                review_count = EXCLUDED.review_count,
                span_count = EXCLUDED.span_count,
                negative_count = EXCLUDED.negative_count,
                positive_count = EXCLUDED.positive_count,
                neutral_count = EXCLUDED.neutral_count,
                mixed_count = EXCLUDED.mixed_count,
                strength_score = EXCLUDED.strength_score,
                negative_strength = EXCLUDED.negative_strength,
                positive_strength = EXCLUDED.positive_strength,
                avg_rating = EXCLUDED.avg_rating,
                i1_count = EXCLUDED.i1_count,
                i2_count = EXCLUDED.i2_count,
                i3_count = EXCLUDED.i3_count,
                cr_better = EXCLUDED.cr_better,
                cr_worse = EXCLUDED.cr_worse,
                cr_same = EXCLUDED.cr_same,
                trust_weighted_strength = EXCLUDED.trust_weighted_strength,
                trust_weighted_negative = EXCLUDED.trust_weighted_negative,
                computed_at = NOW()
            """,
            fact["business_id"],
            fact["place_id"],
            fact["period_date"],
            fact["bucket_type"],
            fact["subject_type"],
            fact["subject_id"],
            fact["taxonomy_version"],
            fact["review_count"],
            fact["span_count"],
            fact["negative_count"],
            fact["positive_count"],
            fact["neutral_count"],
            fact["mixed_count"],
            fact["strength_score"],
            fact["negative_strength"],
            fact["positive_strength"],
            fact.get("avg_rating"),
            fact["i1_count"],
            fact["i2_count"],
            fact["i3_count"],
            fact["cr_better"],
            fact["cr_worse"],
            fact["cr_same"],
            fact["trust_weighted_strength"],
            fact["trust_weighted_negative"],
        )

    async def get_aggregation_data(
        self,
        business_id: str,
        start_date: date,
        end_date: date,
    ) -> list[dict[str, Any]]:
        """Get span data for aggregation within a date range."""
        query = """
            SELECT
                rs.business_id,
                rs.place_id,
                DATE(rs.review_time) as review_date,
                rs.urt_primary,
                rs.valence,
                rs.intensity,
                rs.comparative,
                re.trust_score,
                re.rating
            FROM pipeline.review_spans rs
            JOIN pipeline.reviews_enriched re ON (
                re.source = rs.source
                AND re.review_id = rs.review_id
                AND re.review_version = rs.review_version
            )
            WHERE rs.business_id = $1
              AND rs.is_active = TRUE
              AND DATE(rs.review_time) BETWEEN $2 AND $3
        """
        rows = await self.db.fetch(query, business_id, start_date, end_date)
        return [dict(r) for r in rows]

    async def get_place_ids_for_business(
        self,
        business_id: str,
    ) -> list[str]:
        """Get all place IDs for a business."""
        rows = await self.db.fetch(
            """
            SELECT DISTINCT place_id FROM pipeline.reviews_enriched
            WHERE business_id = $1
            """,
            business_id,
        )
        return [r["place_id"] for r in rows]
