"""
Validation functions for SLTK API Segmentation Tool inputs.

Provides comprehensive validation for segments, merge operations,
split operations, and export paths with clear error messages.
"""

from __future__ import annotations

import logging
import os
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from sltk.api.models import SegmentData

logger = logging.getLogger(__name__)


# ============================================================================
# Validation Result Types
# ============================================================================


@dataclass
class ValidationError:
    """A single validation error with details."""

    field: str
    message: str
    code: str
    value: object = None

    def __str__(self) -> str:
        if self.value is not None:
            return f"{self.field}: {self.message} (got {self.value!r})"
        return f"{self.field}: {self.message}"


@dataclass
class ValidationResult:
    """
    Result of a validation operation.

    Attributes:
        valid: True if validation passed
        errors: List of validation errors (empty if valid)
        warnings: List of non-fatal warnings
    """

    valid: bool
    errors: list[ValidationError]
    warnings: list[str]

    @classmethod
    def success(cls, warnings: list[str] | None = None) -> ValidationResult:
        """Create a successful validation result."""
        return cls(valid=True, errors=[], warnings=warnings or [])

    @classmethod
    def failure(
        cls,
        errors: list[ValidationError],
        warnings: list[str] | None = None,
    ) -> ValidationResult:
        """Create a failed validation result."""
        return cls(valid=False, errors=errors, warnings=warnings or [])

    def __bool__(self) -> bool:
        return self.valid

    def error_messages(self) -> list[str]:
        """Get all error messages as strings."""
        return [str(e) for e in self.errors]

    def to_dict(self) -> dict:
        """Convert to dictionary for API responses."""
        return {
            "valid": self.valid,
            "errors": [
                {"field": e.field, "message": e.message, "code": e.code} for e in self.errors
            ],
            "warnings": self.warnings,
        }


# ============================================================================
# Segment Validation
# ============================================================================


def validate_segment(
    segment: SegmentData,
    max_duration: float | None = None,
    allowed_tiers: list[str] | None = None,
) -> ValidationResult:
    """
    Validate a single segment's data.

    Rules:
    - start >= 0 (non-negative start time)
    - end > start (positive duration)
    - label not empty if provided (can be None)
    - confidence in [0, 1] if provided
    - tier is a non-empty string

    Args:
        segment: The segment to validate
        max_duration: Optional maximum segment duration (seconds)
        allowed_tiers: Optional list of valid tier names

    Returns:
        ValidationResult with any errors found
    """
    errors: list[ValidationError] = []
    warnings: list[str] = []

    # Validate start time
    if segment.start is None:
        errors.append(
            ValidationError(
                field="start",
                message="Start time is required",
                code="required",
            )
        )
    elif not isinstance(segment.start, (int, float)):
        errors.append(
            ValidationError(
                field="start",
                message="Start time must be a number",
                code="invalid_type",
                value=type(segment.start).__name__,
            )
        )
    elif segment.start < 0:
        errors.append(
            ValidationError(
                field="start",
                message="Start time must be non-negative",
                code="negative_time",
                value=segment.start,
            )
        )
    elif not _is_finite(segment.start):
        errors.append(
            ValidationError(
                field="start",
                message="Start time must be a finite number",
                code="non_finite",
                value=segment.start,
            )
        )

    # Validate end time
    if segment.end is None:
        errors.append(
            ValidationError(
                field="end",
                message="End time is required",
                code="required",
            )
        )
    elif not isinstance(segment.end, (int, float)):
        errors.append(
            ValidationError(
                field="end",
                message="End time must be a number",
                code="invalid_type",
                value=type(segment.end).__name__,
            )
        )
    elif _is_nan(segment.end):
        errors.append(
            ValidationError(
                field="end",
                message="End time must not be NaN",
                code="nan_value",
                value=segment.end,
            )
        )

    # Validate start < end (only if both are valid numbers)
    if (
        segment.start is not None
        and segment.end is not None
        and isinstance(segment.start, (int, float))
        and isinstance(segment.end, (int, float))
        and _is_finite(segment.start)
        and not _is_nan(segment.end)
    ):
        if segment.end <= segment.start:
            errors.append(
                ValidationError(
                    field="end",
                    message=f"End time ({segment.end}) must be greater than start time ({segment.start})",
                    code="invalid_range",
                    value={"start": segment.start, "end": segment.end},
                )
            )
        elif max_duration is not None:
            duration = segment.end - segment.start
            if duration > max_duration:
                warnings.append(
                    f"Segment duration ({duration:.2f}s) exceeds maximum ({max_duration:.2f}s)"
                )

    # Validate label (can be None, but not empty string)
    if segment.label is not None:
        if not isinstance(segment.label, str):
            errors.append(
                ValidationError(
                    field="label",
                    message="Label must be a string",
                    code="invalid_type",
                    value=type(segment.label).__name__,
                )
            )
        elif len(segment.label.strip()) == 0:
            errors.append(
                ValidationError(
                    field="label",
                    message="Label cannot be empty or whitespace-only",
                    code="empty_label",
                    value=segment.label,
                )
            )

    # Validate confidence
    if segment.confidence is not None:
        if not isinstance(segment.confidence, (int, float)):
            errors.append(
                ValidationError(
                    field="confidence",
                    message="Confidence must be a number",
                    code="invalid_type",
                    value=type(segment.confidence).__name__,
                )
            )
        elif not _is_finite(segment.confidence):
            errors.append(
                ValidationError(
                    field="confidence",
                    message="Confidence must be a finite number",
                    code="non_finite",
                    value=segment.confidence,
                )
            )
        elif segment.confidence < 0 or segment.confidence > 1:
            warnings.append(
                f"Confidence value ({segment.confidence}) is outside expected [0, 1] range"
            )

    # Validate tier
    if segment.tier is None:
        errors.append(
            ValidationError(
                field="tier",
                message="Tier is required",
                code="required",
            )
        )
    elif not isinstance(segment.tier, str):
        errors.append(
            ValidationError(
                field="tier",
                message="Tier must be a string",
                code="invalid_type",
                value=type(segment.tier).__name__,
            )
        )
    elif len(segment.tier.strip()) == 0:
        errors.append(
            ValidationError(
                field="tier",
                message="Tier cannot be empty",
                code="empty_tier",
                value=segment.tier,
            )
        )
    elif allowed_tiers is not None and segment.tier not in allowed_tiers:
        errors.append(
            ValidationError(
                field="tier",
                message=f"Tier '{segment.tier}' is not in allowed tiers: {allowed_tiers}",
                code="invalid_tier",
                value=segment.tier,
            )
        )

    if errors:
        return ValidationResult.failure(errors, warnings)
    return ValidationResult.success(warnings)


def validate_segment_list(
    segments: list[SegmentData],
    check_overlaps: bool = False,
    check_sorted: bool = False,
    max_duration: float | None = None,
) -> ValidationResult:
    """
    Validate a list of segments.

    Args:
        segments: List of segments to validate
        check_overlaps: If True, warn about overlapping segments on the same tier
        check_sorted: If True, warn if segments are not sorted by start time
        max_duration: Optional maximum individual segment duration

    Returns:
        ValidationResult with any errors found
    """
    errors: list[ValidationError] = []
    warnings: list[str] = []

    if not segments:
        return ValidationResult.success(["Segment list is empty"])

    # Validate each segment
    for i, segment in enumerate(segments):
        result = validate_segment(segment, max_duration=max_duration)
        for error in result.errors:
            # Prefix with index for clarity
            errors.append(
                ValidationError(
                    field=f"segments[{i}].{error.field}",
                    message=error.message,
                    code=error.code,
                    value=error.value,
                )
            )
        warnings.extend(result.warnings)

    # Check if sorted by start time
    if check_sorted and len(segments) > 1:
        starts = [s.start for s in segments if s.start is not None]
        if starts != sorted(starts):
            warnings.append("Segments are not sorted by start time")

    # Check for overlapping segments on the same tier
    if check_overlaps and len(segments) > 1:
        by_tier: dict[str, list[SegmentData]] = {}
        for seg in segments:
            if seg.tier and seg.start is not None and seg.end is not None:
                by_tier.setdefault(seg.tier, []).append(seg)

        for tier, tier_segments in by_tier.items():
            sorted_segs = sorted(tier_segments, key=lambda s: (s.start or 0))
            for i in range(len(sorted_segs) - 1):
                current = sorted_segs[i]
                next_seg = sorted_segs[i + 1]
                if current.end is not None and next_seg.start is not None:
                    if current.end > next_seg.start:
                        warnings.append(
                            f"Overlapping segments on tier '{tier}': "
                            f"[{current.start}-{current.end}] and [{next_seg.start}-{next_seg.end}]"
                        )

    if errors:
        return ValidationResult.failure(errors, warnings)
    return ValidationResult.success(warnings)


# ============================================================================
# Merge Validation
# ============================================================================


def validate_merge_candidates(
    segments: list[SegmentData],
    gap_threshold: float = 0.5,
    same_tier_required: bool = True,
    same_label_required: bool = False,
) -> ValidationResult:
    """
    Validate that segments can be merged.

    Rules:
    - At least 2 segments required
    - Segments must be adjacent (gap < threshold)
    - Segments must be on the same tier (if required)
    - Segments should have the same label (optional)

    Args:
        segments: Segments to merge (should be in temporal order)
        gap_threshold: Maximum gap between segments (seconds)
        same_tier_required: If True, all segments must be on the same tier
        same_label_required: If True, all segments must have the same label

    Returns:
        ValidationResult with any errors found
    """
    errors: list[ValidationError] = []
    warnings: list[str] = []

    # Need at least 2 segments to merge
    if len(segments) < 2:
        errors.append(
            ValidationError(
                field="segments",
                message="At least 2 segments required for merge",
                code="insufficient_segments",
                value=len(segments),
            )
        )
        return ValidationResult.failure(errors)

    # First validate each segment individually
    list_result = validate_segment_list(segments)
    if not list_result.valid:
        return list_result

    # Sort by start time for adjacency check
    sorted_segments = sorted(segments, key=lambda s: s.start)

    # Check adjacency (gap between consecutive segments)
    for i in range(len(sorted_segments) - 1):
        current = sorted_segments[i]
        next_seg = sorted_segments[i + 1]
        gap = next_seg.start - current.end

        if gap > gap_threshold:
            errors.append(
                ValidationError(
                    field=f"segments[{i}]-[{i + 1}]",
                    message=f"Gap ({gap:.3f}s) between segments exceeds threshold ({gap_threshold}s)",
                    code="non_adjacent",
                    value={"gap": gap, "threshold": gap_threshold},
                )
            )
        elif gap < 0:
            warnings.append(f"Segments [{i}] and [{i + 1}] overlap by {abs(gap):.3f}s")

    # Check same tier requirement
    if same_tier_required:
        tiers = set(s.tier for s in segments)
        if len(tiers) > 1:
            errors.append(
                ValidationError(
                    field="tier",
                    message=f"All segments must be on the same tier (found: {tiers})",
                    code="multiple_tiers",
                    value=list(tiers),
                )
            )

    # Check same label requirement
    if same_label_required:
        labels = set(s.label for s in segments if s.label is not None)
        if len(labels) > 1:
            errors.append(
                ValidationError(
                    field="label",
                    message=f"All segments must have the same label (found: {labels})",
                    code="multiple_labels",
                    value=list(labels),
                )
            )
    else:
        # Just warn about different labels
        labels = set(s.label for s in segments if s.label is not None)
        if len(labels) > 1:
            warnings.append(f"Merging segments with different labels: {labels}")

    if errors:
        return ValidationResult.failure(errors, warnings)
    return ValidationResult.success(warnings)


# ============================================================================
# Split Validation
# ============================================================================


def validate_split_point(
    segment: SegmentData,
    split_time: float,
    min_duration: float = 0.01,
) -> ValidationResult:
    """
    Validate that a split point is valid for a segment.

    Rules:
    - split_time must be within the segment (start < split_time < end)
    - Resulting segments must meet minimum duration

    Args:
        segment: The segment to split
        split_time: The time point at which to split
        min_duration: Minimum duration for resulting segments (seconds)

    Returns:
        ValidationResult with any errors found
    """
    errors: list[ValidationError] = []
    warnings: list[str] = []

    # First validate the segment itself
    seg_result = validate_segment(segment)
    if not seg_result.valid:
        return seg_result

    # Validate split_time is a valid number
    if not isinstance(split_time, (int, float)):
        errors.append(
            ValidationError(
                field="split_time",
                message="Split time must be a number",
                code="invalid_type",
                value=type(split_time).__name__,
            )
        )
        return ValidationResult.failure(errors)

    if not _is_finite(split_time):
        errors.append(
            ValidationError(
                field="split_time",
                message="Split time must be a finite number",
                code="non_finite",
                value=split_time,
            )
        )
        return ValidationResult.failure(errors)

    # Check split_time is within segment bounds (exclusive)
    if split_time <= segment.start:
        errors.append(
            ValidationError(
                field="split_time",
                message=f"Split time ({split_time}) must be greater than segment start ({segment.start})",
                code="before_start",
                value={"split_time": split_time, "segment_start": segment.start},
            )
        )

    if split_time >= segment.end:
        errors.append(
            ValidationError(
                field="split_time",
                message=f"Split time ({split_time}) must be less than segment end ({segment.end})",
                code="after_end",
                value={"split_time": split_time, "segment_end": segment.end},
            )
        )

    # Check resulting segment durations
    if not errors:
        left_duration = split_time - segment.start
        right_duration = segment.end - split_time

        if left_duration < min_duration:
            errors.append(
                ValidationError(
                    field="split_time",
                    message=f"Left segment duration ({left_duration:.3f}s) is below minimum ({min_duration}s)",
                    code="duration_too_short",
                    value={"duration": left_duration, "min": min_duration},
                )
            )

        if right_duration < min_duration:
            errors.append(
                ValidationError(
                    field="split_time",
                    message=f"Right segment duration ({right_duration:.3f}s) is below minimum ({min_duration}s)",
                    code="duration_too_short",
                    value={"duration": right_duration, "min": min_duration},
                )
            )

    if errors:
        return ValidationResult.failure(errors, warnings)
    return ValidationResult.success(warnings)


# ============================================================================
# Export Path Validation
# ============================================================================


def validate_export_path(
    path: str,
    allowed_extensions: list[str] | None = None,
    must_not_exist: bool = False,
    create_parents: bool = False,
) -> ValidationResult:
    """
    Validate that an export path is writable and safe.

    Rules:
    - Path must be a valid string
    - Parent directory must exist (or can be created)
    - Path must be writable
    - Extension must be in allowed list (if provided)
    - Optionally check that file doesn't already exist

    Args:
        path: The output path to validate
        allowed_extensions: List of allowed file extensions (e.g., [".eaf", ".json"])
        must_not_exist: If True, fail if file already exists
        create_parents: If True, create parent directories if needed

    Returns:
        ValidationResult with any errors found
    """
    errors: list[ValidationError] = []
    warnings: list[str] = []

    # Validate path is a non-empty string
    if not path:
        errors.append(
            ValidationError(
                field="path",
                message="Output path is required",
                code="required",
            )
        )
        return ValidationResult.failure(errors)

    if not isinstance(path, str):
        errors.append(
            ValidationError(
                field="path",
                message="Output path must be a string",
                code="invalid_type",
                value=type(path).__name__,
            )
        )
        return ValidationResult.failure(errors)

    # Security: Block dangerous characters
    if "\x00" in path:
        errors.append(
            ValidationError(
                field="path",
                message="Path contains invalid null character",
                code="null_byte",
            )
        )
        return ValidationResult.failure(errors)

    # Try to create a Path object
    try:
        path_obj = Path(path).expanduser()
    except Exception as e:
        errors.append(
            ValidationError(
                field="path",
                message=f"Invalid path format: {e}",
                code="invalid_format",
                value=path,
            )
        )
        return ValidationResult.failure(errors)

    # Check for path traversal
    if ".." in path_obj.parts:
        errors.append(
            ValidationError(
                field="path",
                message="Path traversal (..) is not allowed",
                code="path_traversal",
            )
        )
        return ValidationResult.failure(errors)

    # Validate extension
    if allowed_extensions:
        ext = path_obj.suffix.lower()
        allowed_lower = [
            e.lower() if e.startswith(".") else f".{e.lower()}" for e in allowed_extensions
        ]
        if ext not in allowed_lower:
            errors.append(
                ValidationError(
                    field="path",
                    message=f"File extension '{ext}' not allowed. Allowed: {allowed_extensions}",
                    code="invalid_extension",
                    value=ext,
                )
            )

    # Check parent directory
    parent = path_obj.parent
    if not parent.exists():
        if create_parents:
            try:
                parent.mkdir(parents=True, exist_ok=True)
                warnings.append(f"Created parent directory: {parent}")
            except OSError as e:
                errors.append(
                    ValidationError(
                        field="path",
                        message=f"Cannot create parent directory: {e}",
                        code="cannot_create_parent",
                        value=str(parent),
                    )
                )
        else:
            errors.append(
                ValidationError(
                    field="path",
                    message=f"Parent directory does not exist: {parent}",
                    code="parent_not_found",
                    value=str(parent),
                )
            )

    # Check if file already exists
    if path_obj.exists():
        if must_not_exist:
            errors.append(
                ValidationError(
                    field="path",
                    message=f"File already exists: {path}",
                    code="file_exists",
                    value=path,
                )
            )
        else:
            warnings.append(f"File will be overwritten: {path}")

    # Check write permission on parent directory
    if parent.exists():
        if not os.access(parent, os.W_OK):
            errors.append(
                ValidationError(
                    field="path",
                    message=f"No write permission for directory: {parent}",
                    code="no_write_permission",
                    value=str(parent),
                )
            )

    if errors:
        return ValidationResult.failure(errors, warnings)
    return ValidationResult.success(warnings)


# ============================================================================
# Utility Functions
# ============================================================================


def _is_finite(value: float) -> bool:
    """Check if a value is a finite number (not inf or nan)."""
    import math

    return math.isfinite(value)


def _is_nan(value: float) -> bool:
    """Check if a value is NaN."""
    import math

    return math.isnan(value)


# ============================================================================
# Convenience Functions for API Endpoints
# ============================================================================


def require_valid_segment(segment: SegmentData) -> None:
    """
    Validate a segment and raise HTTPException if invalid.

    Use this in API endpoints for quick validation:
        @router.post("/create")
        async def create_segment(segment: SegmentData):
            require_valid_segment(segment)
            # ... proceed with valid segment
    """
    from fastapi import HTTPException

    result = validate_segment(segment)
    if not result.valid:
        raise HTTPException(
            status_code=422,
            detail={
                "message": "Invalid segment data",
                "errors": result.error_messages(),
            },
        )


def require_valid_merge(
    segments: list[SegmentData],
    gap_threshold: float = 0.5,
) -> None:
    """
    Validate merge candidates and raise HTTPException if invalid.
    """
    from fastapi import HTTPException

    result = validate_merge_candidates(segments, gap_threshold=gap_threshold)
    if not result.valid:
        raise HTTPException(
            status_code=422,
            detail={
                "message": "Cannot merge segments",
                "errors": result.error_messages(),
            },
        )


def require_valid_split(
    segment: SegmentData,
    split_time: float,
    min_duration: float = 0.01,
) -> None:
    """
    Validate split operation and raise HTTPException if invalid.
    """
    from fastapi import HTTPException

    result = validate_split_point(segment, split_time, min_duration=min_duration)
    if not result.valid:
        raise HTTPException(
            status_code=422,
            detail={
                "message": "Invalid split operation",
                "errors": result.error_messages(),
            },
        )


def require_valid_export_path(
    path: str,
    allowed_extensions: list[str] | None = None,
) -> Path:
    """
    Validate export path and raise HTTPException if invalid.

    Returns the resolved Path object if valid.
    """
    from fastapi import HTTPException

    result = validate_export_path(path, allowed_extensions=allowed_extensions)
    if not result.valid:
        raise HTTPException(
            status_code=422,
            detail={
                "message": "Invalid export path",
                "errors": result.error_messages(),
            },
        )
    return Path(path).expanduser()
