"""
Supporting data structures for EDS-NLP integration.

This module defines dataclasses and types used throughout the EDS-NLP
integration, including entity extraction results, qualifier detection,
and processing metadata.

Validates: Requirements 2.6, 3.6, 4.3, 5.6
"""

from dataclasses import dataclass, field
from typing import List, Optional, Tuple, Dict
from datetime import datetime


@dataclass
class Span:
    """
    Text span with start and end positions.
    
    Represents a contiguous region of text in a document.
    """
    
    start: int
    """Start position (inclusive) in characters."""
    
    end: int
    """End position (exclusive) in characters."""
    
    def __post_init__(self):
        """Validate span positions."""
        if self.start < 0:
            raise ValueError(f"Span start must be >= 0, got {self.start}")
        if self.end < self.start:
            raise ValueError(f"Span end ({self.end}) must be >= start ({self.start})")
    
    @property
    def length(self) -> int:
        """Length of the span in characters."""
        return self.end - self.start
    
    def overlaps(self, other: "Span") -> bool:
        """Check if this span overlaps with another span."""
        return not (self.end <= other.start or other.end <= self.start)
    
    def contains(self, other: "Span") -> bool:
        """Check if this span fully contains another span."""
        return self.start <= other.start and other.end <= self.end
    
    def to_tuple(self) -> Tuple[int, int]:
        """Convert to (start, end) tuple."""
        return (self.start, self.end)


@dataclass
class QualifierResult:
    """
    Qualifiers detected for a clinical entity.
    
    Represents contextual information about an entity extracted by EDS-NLP
    components (negation, hypothesis, history, family, reported speech).
    
    Validates: Requirements 3.6
    """
    
    negated: bool = False
    """True if entity is negated (e.g., "pas de fièvre")."""
    
    hypothesis: bool = False
    """True if entity is hypothetical/suspected (e.g., "suspicion de pneumonie")."""
    
    historical: bool = False
    """True if entity refers to past medical history (e.g., "antécédent de diabète")."""
    
    family: bool = False
    """True if entity refers to family history (e.g., "père diabétique")."""
    
    reported_speech: bool = False
    """True if entity is reported speech (e.g., "le patient dit avoir mal")."""
    
    negation_cue: Optional[str] = None
    """Specific negation marker text (e.g., "pas de", "absence de")."""
    
    hypothesis_cue: Optional[str] = None
    """Specific hypothesis marker text (e.g., "suspicion", "probable")."""
    
    history_cue: Optional[str] = None
    """Specific history marker text (e.g., "antécédent", "ancien")."""
    
    family_cue: Optional[str] = None
    """Specific family context marker text (e.g., "père", "mère", "famille")."""
    
    reported_speech_cue: Optional[str] = None
    """Specific reported speech marker text (e.g., "dit", "rapporte")."""
    
    confidence: float = 1.0
    """Confidence score for qualifier detection (0.0-1.0)."""
    
    qualifier_spans: List[Span] = field(default_factory=list)
    """Spans of qualifier markers in the text."""
    
    def __post_init__(self):
        """Validate confidence score."""
        if not 0.0 <= self.confidence <= 1.0:
            raise ValueError(f"Confidence must be in [0.0, 1.0], got {self.confidence}")
    
    def has_any_qualifier(self) -> bool:
        """Check if any qualifier is detected."""
        return any([
            self.negated,
            self.hypothesis,
            self.historical,
            self.family,
            self.reported_speech
        ])
    
    def should_exclude_from_coding(self) -> bool:
        """
        Check if entity should be excluded from medical coding.
        
        Family context entities should not be coded for the patient.
        """
        return self.family
    
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        return {
            "negated": self.negated,
            "hypothesis": self.hypothesis,
            "historical": self.historical,
            "family": self.family,
            "reported_speech": self.reported_speech,
            "negation_cue": self.negation_cue,
            "hypothesis_cue": self.hypothesis_cue,
            "history_cue": self.history_cue,
            "family_cue": self.family_cue,
            "reported_speech_cue": self.reported_speech_cue,
            "confidence": self.confidence,
            "qualifier_spans": [span.to_tuple() for span in self.qualifier_spans]
        }


@dataclass
class ExtractedEntity:
    """
    Medical entity extracted by EDS-NLP.
    
    Represents a structured medical entity (diagnostic, medication, procedure,
    date, measurement) extracted from clinical text.
    
    Validates: Requirements 2.6
    """
    
    text: str
    """Original text of the entity."""
    
    entity_type: str
    """Type of entity: diagnostic, medication, procedure, date, measurement."""
    
    span: Span
    """Position of entity in the document."""
    
    confidence: float
    """Confidence score for entity extraction (0.0-1.0)."""
    
    qualifiers: QualifierResult
    """Contextual qualifiers for this entity."""
    
    normalized_text: str
    """Normalized form of the entity text."""
    
    document_id: Optional[str] = None
    """ID of the document containing this entity."""
    
    metadata: Dict[str, any] = field(default_factory=dict)
    """Additional metadata about the entity."""
    
    def __post_init__(self):
        """Validate entity fields."""
        if not self.text:
            raise ValueError("Entity text cannot be empty")
        if not 0.0 <= self.confidence <= 1.0:
            raise ValueError(f"Confidence must be in [0.0, 1.0], got {self.confidence}")
        if not self.entity_type:
            raise ValueError("Entity type cannot be empty")
    
    def should_include_in_coding(self) -> bool:
        """
        Check if entity should be included in medical coding.
        
        Entities with family context should be excluded.
        """
        return not self.qualifiers.should_exclude_from_coding()
    
    def get_adjusted_confidence(self) -> float:
        """
        Get confidence score adjusted for qualifiers.
        
        Qualifiers like negation, hypothesis, and family context
        reduce the confidence score.
        """
        confidence = self.confidence
        
        if self.qualifiers.negated:
            confidence -= 0.1
        if self.qualifiers.hypothesis:
            confidence -= 0.2
        if self.qualifiers.family:
            confidence -= 0.5
        if self.qualifiers.reported_speech:
            confidence -= 0.1
        if self.qualifiers.historical:
            confidence -= 0.05
        
        return max(0.0, min(1.0, confidence))
    
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        return {
            "text": self.text,
            "entity_type": self.entity_type,
            "span": self.span.to_tuple(),
            "confidence": self.confidence,
            "adjusted_confidence": self.get_adjusted_confidence(),
            "qualifiers": self.qualifiers.to_dict(),
            "normalized_text": self.normalized_text,
            "document_id": self.document_id,
            "metadata": self.metadata
        }


@dataclass
class Sentence:
    """
    Sentence segment from eds.sentences component.
    
    Represents a sentence extracted by EDS-NLP's sentence segmentation,
    potentially with sub-sentence propositions.
    
    Validates: Requirements 4.3
    """
    
    text: str
    """Text content of the sentence."""
    
    span: Span
    """Position of sentence in the document."""
    
    propositions: List[str] = field(default_factory=list)
    """Sub-sentence units (propositions) if sentence contains multiple facts."""
    
    document_id: Optional[str] = None
    """ID of the document containing this sentence."""
    
    def __post_init__(self):
        """Validate sentence fields."""
        if not self.text or not self.text.strip():
            raise ValueError("Sentence text cannot be empty or whitespace-only")
    
    def has_propositions(self) -> bool:
        """Check if sentence has been segmented into propositions."""
        return len(self.propositions) > 0
    
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        return {
            "text": self.text,
            "span": self.span.to_tuple(),
            "propositions": self.propositions,
            "document_id": self.document_id
        }


@dataclass
class NormalizedTerm:
    """
    Normalized clinical term.
    
    Represents a clinical term in both original and normalized forms,
    with metadata about the normalization process.
    
    Validates: Requirements 5.6
    """
    
    original: str
    """Original term text."""
    
    normalized: str
    """Normalized term text."""
    
    normalization_steps: List[str] = field(default_factory=list)
    """List of normalization steps applied (e.g., ["lowercase", "accent_removal"])."""
    
    abbreviations_expanded: Dict[str, str] = field(default_factory=dict)
    """Mapping of abbreviations that were expanded."""
    
    def __post_init__(self):
        """Validate term fields."""
        if not self.original:
            raise ValueError("Original term cannot be empty")
        if not self.normalized:
            raise ValueError("Normalized term cannot be empty")
    
    def was_modified(self) -> bool:
        """Check if normalization changed the term."""
        return self.original != self.normalized
    
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        return {
            "original": self.original,
            "normalized": self.normalized,
            "was_modified": self.was_modified(),
            "normalization_steps": self.normalization_steps,
            "abbreviations_expanded": self.abbreviations_expanded
        }


@dataclass
class ProcessingResult:
    """
    Result of EDS-NLP document processing.
    
    Contains all extracted entities, sentences, and metadata from
    processing a single document through the EDS-NLP pipeline.
    
    Validates: Requirements 2.6, 3.6, 4.3
    """
    
    document_id: str
    """ID of the processed document."""
    
    entities: List[ExtractedEntity]
    """List of extracted medical entities."""
    
    sentences: List[Sentence]
    """List of segmented sentences."""
    
    processing_time: float
    """Time taken to process the document (seconds)."""
    
    success: bool
    """Whether processing completed successfully."""
    
    error: Optional[str] = None
    """Error message if processing failed."""
    
    error_details: Dict[str, any] = field(default_factory=dict)
    """Additional error context."""
    
    metadata: Dict[str, any] = field(default_factory=dict)
    """Additional processing metadata."""
    
    timestamp: datetime = field(default_factory=datetime.utcnow)
    """Timestamp when processing completed."""
    
    def __post_init__(self):
        """Validate processing result."""
        if not self.document_id:
            raise ValueError("Document ID cannot be empty")
        if self.processing_time < 0:
            raise ValueError(f"Processing time must be >= 0, got {self.processing_time}")
    
    def get_entity_count_by_type(self) -> Dict[str, int]:
        """Get count of entities by type."""
        counts = {}
        for entity in self.entities:
            entity_type = entity.entity_type
            counts[entity_type] = counts.get(entity_type, 0) + 1
        return counts
    
    def get_entities_for_coding(self) -> List[ExtractedEntity]:
        """Get entities that should be included in medical coding."""
        return [e for e in self.entities if e.should_include_in_coding()]
    
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        return {
            "document_id": self.document_id,
            "entities": [e.to_dict() for e in self.entities],
            "sentences": [s.to_dict() for s in self.sentences],
            "processing_time": self.processing_time,
            "success": self.success,
            "error": self.error,
            "error_details": self.error_details,
            "metadata": self.metadata,
            "timestamp": self.timestamp.isoformat(),
            "entity_counts": self.get_entity_count_by_type()
        }


@dataclass
class ExtractionResult:
    """
    Result of clinical fact extraction.
    
    High-level result containing extracted clinical facts and metadata
    about the extraction process (EDS-NLP or fallback).
    """
    
    facts: List[any]  # List[ClinicalFact] - avoiding circular import
    """List of extracted clinical facts."""
    
    extraction_method: str
    """Method used for extraction: "edsnlp" or "regex"."""
    
    processing_time: float
    """Total time taken for extraction (seconds)."""
    
    fallback_used: bool
    """Whether fallback to regex extraction was used."""
    
    document_count: int = 0
    """Number of documents processed."""
    
    error: Optional[str] = None
    """Error message if extraction failed."""
    
    error_details: Dict[str, any] = field(default_factory=dict)
    """Additional error context."""
    
    metadata: Dict[str, any] = field(default_factory=dict)
    """Additional extraction metadata."""
    
    timestamp: datetime = field(default_factory=datetime.utcnow)
    """Timestamp when extraction completed."""
    
    def __post_init__(self):
        """Validate extraction result."""
        if self.extraction_method not in ["edsnlp", "regex"]:
            raise ValueError(f"Invalid extraction method: {self.extraction_method}")
        if self.processing_time < 0:
            raise ValueError(f"Processing time must be >= 0, got {self.processing_time}")
    
    def was_successful(self) -> bool:
        """Check if extraction was successful."""
        return self.error is None
    
    def to_dict(self) -> dict:
        """Convert to dictionary for serialization."""
        return {
            "fact_count": len(self.facts),
            "extraction_method": self.extraction_method,
            "processing_time": self.processing_time,
            "fallback_used": self.fallback_used,
            "document_count": self.document_count,
            "error": self.error,
            "error_details": self.error_details,
            "metadata": self.metadata,
            "timestamp": self.timestamp.isoformat(),
            "success": self.was_successful()
        }
