"""
Validation rules for pipeline stages.

Implements validation rules V1.x, V2.x, V3.x, V4.x from the contracts.
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any, Callable

from reviewiq_pipeline.contracts import (
    ValidationError,
    ValidationResult,
)
from reviewiq_pipeline.services.text_processor import is_valid_iso639, is_valid_sha256

if TYPE_CHECKING:
    from reviewiq_pipeline.contracts import (
        FactRecord,
        NormalizedReview,
        RoutedSpan,
        Stage1Output,
        Stage2Output,
        Stage3Output,
        Stage4Output,
    )
    from reviewiq_pipeline.db.connection import DatabasePool

# URT code pattern
URT_CODE_PATTERN = re.compile(r"^[OPJEAVR][1-4]\.[0-9]{2}$")

# Issue ID pattern
ISSUE_ID_PATTERN = re.compile(r"^ISS-[a-f0-9]{16}$")

# Valid enum values
VALID_VALENCES = {"V+", "V-", "V0", "V±"}
VALID_INTENSITIES = {"I1", "I2", "I3"}
VALID_SPECIFICITIES = {"S1", "S2", "S3"}
VALID_ACTIONABILITIES = {"A1", "A2", "A3"}
VALID_TEMPORALS = {"TC", "TR", "TH", "TF"}
VALID_EVIDENCES = {"ES", "EI", "EC"}
VALID_COMPARATIVES = {"CR-N", "CR-B", "CR-W", "CR-S"}


def _has_control_chars(text: str) -> bool:
    """Check if text contains control characters."""
    return bool(re.search(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]", text))


class Stage1Validator:
    """Validator for Stage 1 output."""

    def validate(self, output: Stage1Output) -> ValidationResult:
        """
        Validate Stage 1 output.

        Rules:
        - V1.1: text is non-empty string
        - V1.2: text_normalized contains no control chars
        - V1.3: content_hash is 64-char hex
        - V1.4: review_version >= 1
        - V1.5: text_language is valid ISO 639-1
        - V1.6: raw_id references valid reviews_raw row (requires DB)
        """
        errors: list[ValidationError] = []

        for review in output["reviews_normalized"]:
            review_id = review["review_id"]

            # V1.1: Non-empty text
            if not review.get("text") or not review["text"].strip():
                errors.append(ValidationError(
                    rule="V1.1",
                    identifier=review_id,
                    message="Empty text",
                ))

            # V1.2: No control characters in normalized text
            if review.get("text_normalized") and _has_control_chars(review["text_normalized"]):
                errors.append(ValidationError(
                    rule="V1.2",
                    identifier=review_id,
                    message="Control chars in normalized text",
                ))

            # V1.3: Valid content hash
            if not is_valid_sha256(review.get("content_hash", "")):
                errors.append(ValidationError(
                    rule="V1.3",
                    identifier=review_id,
                    message=f"Invalid content hash: {review.get('content_hash', '')[:20]}...",
                ))

            # V1.4: Version >= 1
            if review.get("review_version", 0) < 1:
                errors.append(ValidationError(
                    rule="V1.4",
                    identifier=review_id,
                    message=f"Invalid version: {review.get('review_version')}",
                ))

            # V1.5: Valid language code
            if not is_valid_iso639(review.get("text_language", "")):
                errors.append(ValidationError(
                    rule="V1.5",
                    identifier=review_id,
                    message=f"Invalid language: {review.get('text_language')}",
                ))

        return ValidationResult(
            stage="stage1",
            passed=len(errors) == 0,
            error_count=len(errors),
            errors=errors,
        )


class Stage2Validator:
    """Validator for Stage 2 output."""

    def validate(
        self,
        output: Stage2Output,
        input_reviews: dict[tuple[str, str, int], dict[str, Any]] | None = None,
    ) -> ValidationResult:
        """
        Validate Stage 2 output.

        Rules:
        - V2.1: urt_primary matches pattern
        - V2.2: urt_secondary has max 2 elements
        - V2.3: valence is valid enum
        - V2.4: intensity is valid enum
        - V2.5: span_end > span_start
        - V2.6: span_text matches text[span_start:span_end]
        - V2.7: spans do not overlap
        - V2.8: exactly one is_primary per review
        - V2.9: trust_score between 0.2 and 1.0
        - V2.10: embedding is 384-dim array
        - V2.11: usn matches profile-specific regex
        - V2.12: related_span_index references valid span

        Args:
            output: Stage 2 output to validate
            input_reviews: Optional dict mapping (source, review_id, version) -> review data
        """
        errors: list[ValidationError] = []

        for review in output["reviews_classified"]:
            review_id = review["review_id"]

            # V2.1: Valid URT code
            if not URT_CODE_PATTERN.match(review.get("urt_primary", "")):
                errors.append(ValidationError(
                    rule="V2.1",
                    identifier=review_id,
                    message=f"Invalid URT code: {review.get('urt_primary')}",
                ))

            # V2.2: Max 2 secondary codes
            if len(review.get("urt_secondary", [])) > 2:
                errors.append(ValidationError(
                    rule="V2.2",
                    identifier=review_id,
                    message=f"Too many secondary codes: {len(review.get('urt_secondary', []))}",
                ))

            # V2.3: Valid valence
            if review.get("valence") not in VALID_VALENCES:
                errors.append(ValidationError(
                    rule="V2.3",
                    identifier=review_id,
                    message=f"Invalid valence: {review.get('valence')}",
                ))

            # V2.4: Valid intensity
            if review.get("intensity") not in VALID_INTENSITIES:
                errors.append(ValidationError(
                    rule="V2.4",
                    identifier=review_id,
                    message=f"Invalid intensity: {review.get('intensity')}",
                ))

            # V2.9: Trust score bounds
            trust = review.get("trust_score", 0)
            if not (0.2 <= trust <= 1.0):
                errors.append(ValidationError(
                    rule="V2.9",
                    identifier=review_id,
                    message=f"Trust score out of bounds: {trust}",
                ))

            # V2.10: Embedding dimension
            embedding = review.get("embedding", [])
            if embedding and len(embedding) != 384:
                errors.append(ValidationError(
                    rule="V2.10",
                    identifier=review_id,
                    message=f"Invalid embedding dimension: {len(embedding)}",
                ))

            # Validate spans
            spans = review.get("spans", [])
            primary_count = 0
            span_ranges: list[tuple[int, int]] = []

            # Get original text if available
            original_text = ""
            if input_reviews:
                key = (review["source"], review["review_id"], review["review_version"])
                original_text = input_reviews.get(key, {}).get("text", "")

            for span in spans:
                span_id = span.get("span_id", f"{review_id}:span")

                # V2.5: Valid bounds
                start = span.get("span_start", 0)
                end = span.get("span_end", 0)
                if end <= start:
                    errors.append(ValidationError(
                        rule="V2.5",
                        identifier=span_id,
                        message=f"Invalid bounds: {start}:{end}",
                    ))

                # V2.6: Text matches (if we have original)
                if original_text and span.get("span_text"):
                    expected = original_text[start:end]
                    # Allow whitespace normalization
                    expected_norm = " ".join(expected.split())
                    actual_norm = " ".join(span["span_text"].split())
                    if expected_norm != actual_norm:
                        errors.append(ValidationError(
                            rule="V2.6",
                            identifier=span_id,
                            message=f"Text mismatch at {start}:{end}",
                        ))

                # V2.7: Check overlap
                for prev_start, prev_end in span_ranges:
                    if not (end <= prev_start or start >= prev_end):
                        errors.append(ValidationError(
                            rule="V2.7",
                            identifier=span_id,
                            message="Overlapping span",
                        ))
                        break
                span_ranges.append((start, end))

                # V2.8: Count primaries
                if span.get("is_primary"):
                    primary_count += 1

                # V2.12: Valid related_span_index
                related_idx = span.get("related_span_index")
                if related_idx is not None:
                    if related_idx < 0 or related_idx >= len(spans):
                        errors.append(ValidationError(
                            rule="V2.12",
                            identifier=span_id,
                            message=f"Invalid related_span_index: {related_idx}",
                        ))
                    elif related_idx == span.get("span_index"):
                        errors.append(ValidationError(
                            rule="V2.12",
                            identifier=span_id,
                            message="Self-referencing span",
                        ))

            # V2.8: Exactly one primary
            if primary_count != 1:
                errors.append(ValidationError(
                    rule="V2.8",
                    identifier=review_id,
                    message=f"Primary span count: {primary_count}",
                ))

        return ValidationResult(
            stage="stage2",
            passed=len(errors) == 0,
            error_count=len(errors),
            errors=errors,
        )


class Stage3Validator:
    """Validator for Stage 3 output."""

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

    async def validate(self, output: Stage3Output) -> ValidationResult:
        """
        Validate Stage 3 output.

        Rules:
        - V3.1: issue_id matches pattern
        - V3.2: routing_key is non-empty
        - V3.3: span not already linked to different issue
        - V3.4: issue exists in issues table
        - V3.5: only V-/V± spans create issues
        """
        errors: list[ValidationError] = []

        for routed in output["routed_spans"]:
            span_id = routed["span_id"]

            # V3.1: Valid issue ID format
            if not ISSUE_ID_PATTERN.match(routed.get("issue_id", "")):
                errors.append(ValidationError(
                    rule="V3.1",
                    identifier=span_id,
                    message=f"Invalid issue_id: {routed.get('issue_id')}",
                ))

            # V3.2: Non-empty routing key
            if not routed.get("routing_key"):
                errors.append(ValidationError(
                    rule="V3.2",
                    identifier=span_id,
                    message="Empty routing key",
                ))

            # V3.3, V3.4: Require database for these checks
            if self.db:
                # V3.3: Check no duplicate routing
                existing = await self.db.fetchval(
                    "SELECT issue_id FROM issue_spans WHERE span_id = $1",
                    span_id,
                )
                if existing and existing != routed["issue_id"]:
                    errors.append(ValidationError(
                        rule="V3.3",
                        identifier=span_id,
                        message=f"Already routed to {existing}",
                    ))

                # V3.4: Issue exists
                issue_exists = await self.db.fetchval(
                    "SELECT 1 FROM issues WHERE issue_id = $1",
                    routed["issue_id"],
                )
                if not issue_exists:
                    errors.append(ValidationError(
                        rule="V3.4",
                        identifier=span_id,
                        message=f"Issue not found: {routed['issue_id']}",
                    ))

        return ValidationResult(
            stage="stage3",
            passed=len(errors) == 0,
            error_count=len(errors),
            errors=errors,
        )

    def validate_sync(self, output: Stage3Output) -> ValidationResult:
        """Synchronous validation without database checks."""
        errors: list[ValidationError] = []

        for routed in output["routed_spans"]:
            span_id = routed["span_id"]

            # V3.1: Valid issue ID format
            if not ISSUE_ID_PATTERN.match(routed.get("issue_id", "")):
                errors.append(ValidationError(
                    rule="V3.1",
                    identifier=span_id,
                    message=f"Invalid issue_id: {routed.get('issue_id')}",
                ))

            # V3.2: Non-empty routing key
            if not routed.get("routing_key"):
                errors.append(ValidationError(
                    rule="V3.2",
                    identifier=span_id,
                    message="Empty routing key",
                ))

        return ValidationResult(
            stage="stage3",
            passed=len(errors) == 0,
            error_count=len(errors),
            errors=errors,
        )


class Stage4Validator:
    """Validator for Stage 4 output."""

    def validate(self, output: Stage4Output) -> ValidationResult:
        """
        Validate Stage 4 output.

        Rules:
        - V4.1: place_id is valid or 'ALL'
        - V4.2: period_date matches bucket
        - V4.3: span_count >= review_count
        - V4.4: valence counts sum to span_count
        - V4.5: intensity counts sum to span_count
        - V4.6: strength_score >= 0
        - V4.7: avg_rating between 1.0 and 5.0 (or NULL)
        """
        errors: list[ValidationError] = []

        for fact in output["facts_written"]:
            fact_id = f"{fact['subject_type']}:{fact['subject_id']}"

            # V4.1: Valid place_id
            place_id = fact.get("place_id", "")
            if not place_id:
                errors.append(ValidationError(
                    rule="V4.1",
                    identifier=fact_id,
                    message="Empty place_id",
                ))

            # V4.3: span_count >= review_count
            if fact.get("span_count", 0) < fact.get("review_count", 0):
                errors.append(ValidationError(
                    rule="V4.3",
                    identifier=fact_id,
                    message=f"span_count ({fact.get('span_count')}) < review_count ({fact.get('review_count')})",
                ))

            # V4.4: Valence sum
            valence_sum = (
                fact.get("negative_count", 0) +
                fact.get("positive_count", 0) +
                fact.get("neutral_count", 0) +
                fact.get("mixed_count", 0)
            )
            if valence_sum != fact.get("span_count", 0):
                errors.append(ValidationError(
                    rule="V4.4",
                    identifier=fact_id,
                    message=f"Valence sum {valence_sum} != span_count {fact.get('span_count')}",
                ))

            # V4.5: Intensity sum
            intensity_sum = (
                fact.get("i1_count", 0) +
                fact.get("i2_count", 0) +
                fact.get("i3_count", 0)
            )
            if intensity_sum != fact.get("span_count", 0):
                errors.append(ValidationError(
                    rule="V4.5",
                    identifier=fact_id,
                    message=f"Intensity sum {intensity_sum} != span_count {fact.get('span_count')}",
                ))

            # V4.6: Non-negative strength
            if fact.get("strength_score", 0) < 0:
                errors.append(ValidationError(
                    rule="V4.6",
                    identifier=fact_id,
                    message=f"Negative strength_score: {fact.get('strength_score')}",
                ))

            # V4.7: Rating bounds
            avg_rating = fact.get("avg_rating")
            if avg_rating is not None and not (1.0 <= avg_rating <= 5.0):
                errors.append(ValidationError(
                    rule="V4.7",
                    identifier=fact_id,
                    message=f"Invalid avg_rating: {avg_rating}",
                ))

        return ValidationResult(
            stage="stage4",
            passed=len(errors) == 0,
            error_count=len(errors),
            errors=errors,
        )


# Convenience functions
def validate_stage1_output(output: Stage1Output) -> ValidationResult:
    """Validate Stage 1 output."""
    return Stage1Validator().validate(output)


def validate_stage2_output(
    output: Stage2Output,
    input_reviews: dict[tuple[str, str, int], dict[str, Any]] | None = None,
) -> ValidationResult:
    """Validate Stage 2 output."""
    return Stage2Validator().validate(output, input_reviews)


async def validate_stage3_output(
    output: Stage3Output,
    db: DatabasePool | None = None,
) -> ValidationResult:
    """Validate Stage 3 output."""
    validator = Stage3Validator(db)
    if db:
        return await validator.validate(output)
    return validator.validate_sync(output)


def validate_stage4_output(output: Stage4Output) -> ValidationResult:
    """Validate Stage 4 output."""
    return Stage4Validator().validate(output)
