"""
API REST pour l'interface TIM.

Cette API fournit les endpoints pour le workflow TIM :
- Afficher les codes proposés avec justifications
- Corriger un code
- Valider un dossier
- Ajouter des commentaires
- Exporter l'audit

Exigences : 10.1, 10.6, 10.7, 10.8, 10.9
"""

from datetime import datetime
from typing import List, Optional
from pathlib import Path

from fastapi import FastAPI, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

from pipeline_mco_pmsi.audit.audit_logger import AuditLogger
from pipeline_mco_pmsi.database.base import get_db
from pipeline_mco_pmsi.database.models import StayDB, CodeDB
from pipeline_mco_pmsi.models.coding import Code, CodingProposal
from pipeline_mco_pmsi.models.metadata import VersionInfo
from pipeline_mco_pmsi.security.access_control import AccessControl, Permission
from pipeline_mco_pmsi.security.encryption import EncryptionKey

# Créer l'application FastAPI
app = FastAPI(
    title="Pipeline MCO PMSI - API TIM",
    description="API REST pour l'interface TIM du pipeline de codage MCO PMSI",
    version="1.0.0",
)

# Configuration CORS pour permettre les requêtes depuis le frontend
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # En production, spécifier les origines autorisées
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Middleware pour les headers de sécurité (CSP)
# Validates: Requirements 16.7
@app.middleware("http")
async def add_security_headers(request, call_next):
    """Ajouter les headers de sécurité CSP."""
    response = await call_next(request)
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com; "
        "style-src 'self' 'unsafe-inline'; "
        "img-src 'self' data:; "
        "font-src 'self'; "
        "connect-src 'self' https://cdnjs.cloudflare.com"
    )
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    return response

# Rate limiting simple (100 requêtes par minute par IP - augmenté pour le développement)
# Validates: Requirements 16.1
from collections import defaultdict
from time import time

rate_limit_store = defaultdict(list)

@app.middleware("http")
async def rate_limit_middleware(request, call_next):
    """Rate limiting simple."""
    client_ip = request.client.host
    current_time = time()
    
    # Nettoyer les anciennes requêtes (> 1 minute)
    rate_limit_store[client_ip] = [
        req_time for req_time in rate_limit_store[client_ip]
        if current_time - req_time < 60
    ]
    
    # Vérifier le nombre de requêtes (augmenté à 100 pour le développement)
    if len(rate_limit_store[client_ip]) >= 100:
        raise HTTPException(status_code=429, detail="Too many requests")
    
    # Ajouter la requête actuelle
    rate_limit_store[client_ip].append(current_time)
    
    response = await call_next(request)
    return response

# Monter les fichiers statiques
static_dir = Path(__file__).parent / "static"
if static_dir.exists():
    app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")

# Modèles Pydantic pour les requêtes/réponses


class CodingProposalResponse(BaseModel):
    """Réponse contenant la proposition de codage."""

    stay_id: str
    age: Optional[int] = None
    patient_id: Optional[str] = None
    admission_date: Optional[str] = None
    discharge_date: Optional[str] = None
    birth_date: Optional[str] = None
    sex: Optional[str] = None
    weight: Optional[float] = None
    height: Optional[float] = None
    specialty: Optional[str] = None
    dp: Optional[Code] = None
    dr: Optional[Code] = None
    das: List[Code] = []
    ccam: List[Code] = []
    reasoning: str
    confidence_scores: dict = Field(default_factory=dict)


class CodeCorrectionRequest(BaseModel):
    """Requête pour corriger un code."""

    stay_id: str
    original_code: str
    corrected_code: str
    corrected_label: Optional[str] = None
    comment: Optional[str] = None


class ValidationRequest(BaseModel):
    """Requête pour valider un dossier."""

    stay_id: str
    validation_status: str  # "accepted", "rejected", "needs_review"
    comment: Optional[str] = None


class CommentRequest(BaseModel):
    """Requête pour ajouter un commentaire."""

    stay_id: str
    code: str
    comment: str


class AuditExportRequest(BaseModel):
    """Requête pour exporter l'audit."""

    stay_id: str
    include_pii: bool = False
    encrypt: bool = True


# Dépendances


def get_audit_logger(db: Session = Depends(get_db)) -> AuditLogger:
    """Dépendance pour obtenir l'AuditLogger."""
    return AuditLogger(db_session=db)


def get_access_control() -> AccessControl:
    """Dépendance pour obtenir l'AccessControl."""
    return AccessControl()


# Endpoints


@app.get("/")
async def root():
    """Endpoint racine - redirige vers l'interface web."""
    return FileResponse(str(Path(__file__).parent / "static" / "index.html"))


@app.get("/stays/{stay_id}/coding-proposal", response_model=CodingProposalResponse)
async def get_coding_proposal(
    stay_id: str,
    db: Session = Depends(get_db),
):
    """
    Affiche les codes proposés avec scores de confiance et justifications.

    Exigences : 10.1
    """
    # Récupérer le séjour
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")

    # Récupérer les codes proposés
    codes = db.query(CodeDB).filter(
        CodeDB.stay_id == stay.id, CodeDB.status == "proposed"
    ).all()

    if not codes:
        raise HTTPException(
            status_code=404, detail=f"No coding proposal found for stay {stay_id}"
        )

    # Organiser les codes par type
    dp = None
    dr = None
    das = []
    ccam = []

    for code_db in codes:
        # Charger les preuves
        evidences = []
        for ev_db in code_db.evidences:
            from pipeline_mco_pmsi.models.clinical import Evidence, Span

            evidences.append(
                Evidence(
                    document_id=ev_db.document.document_id,
                    span=Span(start=ev_db.span_start, end=ev_db.span_end),
                    text=ev_db.text,
                    context=ev_db.context,
                )
            )

        code = Code(
            code=code_db.code,
            label=code_db.label,
            type=code_db.type,
            evidence=evidences,
            confidence=code_db.confidence,
            reasoning=code_db.reasoning,
            referentiel_version=code_db.referentiel_version,
        )

        if code.type == "dp":
            dp = code
        elif code.type == "dr":
            dr = code
        elif code.type == "das":
            das.append(code)
        elif code.type == "ccam":
            ccam.append(code)

    # Calculer les scores de confiance
    confidence_scores = {
        "dp": dp.confidence if dp else 0.0,
        "dr": dr.confidence if dr else 0.0,
        "das_avg": sum(c.confidence for c in das) / len(das) if das else 0.0,
        "ccam_avg": sum(c.confidence for c in ccam) / len(ccam) if ccam else 0.0,
    }

    # Extraire les informations patient depuis les documents si non disponibles
    age = stay.age
    sex = stay.sex
    birth_date = None
    
    if not age or not sex:
        import re
        from datetime import datetime
        
        for doc in stay.documents:
            content = doc.content
            
            # Extraire la date de naissance
            if not birth_date:
                birth_match = re.search(r'(?:Né|Date de naissance).*?(\d{2})/(\d{2})/(\d{4})', content, re.IGNORECASE)
                if birth_match:
                    day, month, year = birth_match.groups()
                    birth_date = f"{year}-{month}-{day}"
                    
                    # Calculer l'âge
                    if stay.admission_date:
                        birth_dt = datetime.strptime(birth_date, "%Y-%m-%d")
                        admission_dt = stay.admission_date
                        age = admission_dt.year - birth_dt.year
                        if (admission_dt.month, admission_dt.day) < (birth_dt.month, birth_dt.day):
                            age -= 1
            
            # Extraire le sexe
            if not sex:
                if re.search(r'\bMonsieur\b', content, re.IGNORECASE):
                    sex = "M"
                elif re.search(r'\bMadame\b', content, re.IGNORECASE):
                    sex = "F"
                elif re.search(r'Sexe\s*:\s*Masculin', content, re.IGNORECASE):
                    sex = "M"
                elif re.search(r'Sexe\s*:\s*Féminin', content, re.IGNORECASE):
                    sex = "F"
    
    return CodingProposalResponse(
        stay_id=stay_id,
        age=age,
        patient_id=stay.stay_id,  # Utiliser stay_id comme patient_id
        admission_date=stay.admission_date.isoformat() if stay.admission_date else None,
        discharge_date=stay.discharge_date.isoformat() if stay.discharge_date else None,
        birth_date=birth_date,
        sex=sex,
        weight=None,  # Pas disponible dans StayDB
        height=None,  # Pas disponible dans StayDB
        specialty=stay.specialty,
        dp=dp,
        dr=dr,
        das=das,
        ccam=ccam,
        reasoning="Proposition de codage basée sur les faits cliniques extraits",
        confidence_scores=confidence_scores,
    )


@app.post("/stays/{stay_id}/correct-code")
async def correct_code(
    stay_id: str,
    correction: CodeCorrectionRequest,
    db: Session = Depends(get_db),
    audit_logger: AuditLogger = Depends(get_audit_logger),
):
    """
    Corrige un code avec enregistrement de l'horodatage et de l'utilisateur.

    Exigences : 10.6
    """
    # Vérifier que le séjour existe
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")

    # Vérifier que le stay_id correspond
    if correction.stay_id != stay_id:
        raise HTTPException(status_code=400, detail="Stay ID mismatch")

    # Récupérer le code à corriger
    code_db = (
        db.query(CodeDB)
        .filter(CodeDB.stay_id == stay.id, CodeDB.code == correction.original_code)
        .first()
    )

    if not code_db:
        raise HTTPException(
            status_code=404,
            detail=f"Code {correction.original_code} not found for stay {stay_id}",
        )

    # Créer la correction TIM dans la base
    from pipeline_mco_pmsi.database.models import TIMCorrectionDB

    tim_correction = TIMCorrectionDB(
        original_code_id=code_db.id,
        corrected_code=correction.corrected_code,
        corrected_label=correction.corrected_label or "",
        corrected_type=code_db.type,  # Garder le même type que le code original
        user_id="tim_user",  # TODO: Récupérer depuis l'authentification
        comment=correction.comment,
        timestamp=datetime.utcnow(),
    )
    db.add(tim_correction)
    db.commit()

    # Enregistrer dans l'audit
    audit_logger.log_tim_correction(
        stay_id=stay_id,
        original_code=correction.original_code,
        corrected_code=correction.corrected_code,
        user_id="tim_user",  # TODO: Récupérer depuis l'authentification
        comment=correction.comment,
    )

    return {
        "message": "Code corrected successfully",
        "stay_id": stay_id,
        "original_code": correction.original_code,
        "corrected_code": correction.corrected_code,
        "timestamp": tim_correction.timestamp.isoformat(),
    }


@app.post("/stays/{stay_id}/validate")
async def validate_stay(
    stay_id: str,
    validation: ValidationRequest,
    db: Session = Depends(get_db),
    audit_logger: AuditLogger = Depends(get_audit_logger),
):
    """
    Valide un dossier avec enregistrement de l'horodatage et de l'utilisateur.

    Exigences : 10.7
    """
    # Vérifier que le séjour existe
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")

    # Vérifier que le stay_id correspond
    if validation.stay_id != stay_id:
        raise HTTPException(status_code=400, detail="Stay ID mismatch")

    # Enregistrer la validation dans l'audit
    audit_logger.log_validation(
        stay_id=stay_id,
        user_id="tim_user",  # TODO: Récupérer depuis l'authentification
        validation_status=validation.validation_status,
        comment=validation.comment,
    )

    # Mettre à jour le statut du séjour
    stay.status = validation.validation_status
    db.commit()

    return {
        "message": "Stay validated successfully",
        "stay_id": stay_id,
        "validation_status": validation.validation_status,
        "timestamp": datetime.utcnow().isoformat(),
    }


@app.post("/stays/{stay_id}/comment")
async def add_comment(
    stay_id: str,
    comment_req: CommentRequest,
    db: Session = Depends(get_db),
):
    """
    Ajoute un commentaire à un code.

    Exigences : 10.8
    """
    # Vérifier que le séjour existe
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")

    # Récupérer le code
    code_db = (
        db.query(CodeDB)
        .filter(CodeDB.stay_id == stay.id, CodeDB.code == comment_req.code)
        .first()
    )

    if not code_db:
        raise HTTPException(
            status_code=404,
            detail=f"Code {comment_req.code} not found for stay {stay_id}",
        )

    # Ajouter le commentaire (on peut utiliser TIMCorrectionDB pour stocker les commentaires)
    from pipeline_mco_pmsi.database.models import TIMCorrectionDB

    comment_db = TIMCorrectionDB(
        original_code_id=code_db.id,
        corrected_code=code_db.code,  # Pas de correction, juste un commentaire
        corrected_label=code_db.label,
        corrected_type=code_db.type,
        user_id="tim_user",  # TODO: Récupérer depuis l'authentification
        comment=comment_req.comment,
        timestamp=datetime.utcnow(),
    )
    db.add(comment_db)
    db.commit()

    return {
        "message": "Comment added successfully",
        "stay_id": stay_id,
        "code": comment_req.code,
        "comment": comment_req.comment,
        "timestamp": comment_db.timestamp.isoformat(),
    }


@app.post("/stays/{stay_id}/audit/export")
async def export_audit(
    stay_id: str,
    export_req: AuditExportRequest,
    db: Session = Depends(get_db),
    audit_logger: AuditLogger = Depends(get_audit_logger),
):
    """
    Exporte la piste d'audit pour un séjour.

    Exigences : 10.9
    """
    # Vérifier que le séjour existe
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")

    try:
        if export_req.encrypt:
            # Générer une clé de chiffrement
            encryption_key = EncryptionKey.generate()

            # Exporter avec chiffrement
            encrypted_data = audit_logger.export_audit_trail_encrypted(
                stay_id=stay_id,
                encryption_key=encryption_key,
                include_pii=export_req.include_pii,
            )

            # Retourner les données chiffrées en base64
            import base64

            return {
                "stay_id": stay_id,
                "encrypted": True,
                "data": base64.b64encode(encrypted_data).decode("ascii"),
                "encryption_key": encryption_key.to_string(),
                "export_date": datetime.utcnow().isoformat(),
            }
        else:
            # Exporter sans chiffrement
            audit_trail = audit_logger.export_audit_trail(
                stay_id=stay_id, include_pii=export_req.include_pii
            )

            return {
                "stay_id": stay_id,
                "encrypted": False,
                "data": audit_trail.model_dump(mode="json"),
                "export_date": datetime.utcnow().isoformat(),
            }

    except Exception as e:
        raise HTTPException(
            status_code=500, detail=f"Failed to export audit trail: {str(e)}"
        )


@app.get("/stays/{stay_id}/clinical-facts")
async def get_clinical_facts(
    stay_id: str,
    db: Session = Depends(get_db),
):
    """
    Récupère les faits cliniques d'un séjour organisés par catégorie.
    
    Exigences: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6
    """
    # Récupérer le séjour
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")
    
    # Récupérer les faits cliniques
    from pipeline_mco_pmsi.database.models import ClinicalFactDB
    
    facts = db.query(ClinicalFactDB).filter(
        ClinicalFactDB.stay_id == stay.id
    ).all()
    
    # Organiser par catégorie
    organized_facts = {
        "symptoms": [],
        "diagnoses": [],
        "treatments": [],
        "history": []
    }
    
    for fact_db in facts:
        # Récupérer les codes liés
        linked_codes = []
        if fact_db.linked_codes:
            for code_db in fact_db.linked_codes:
                linked_codes.append(code_db.code)
        
        fact = {
            "id": str(fact_db.id),
            "text": fact_db.text,
            "document_id": fact_db.document.document_id if fact_db.document else None,
            "span": {
                "start": fact_db.span_start,
                "end": fact_db.span_end
            },
            "linked_codes": linked_codes
        }
        
        category = fact_db.category.lower() if fact_db.category else "symptoms"
        if category in organized_facts:
            organized_facts[category].append(fact)
        else:
            # Par défaut, mettre dans symptoms
            organized_facts["symptoms"].append(fact)
    
    return {
        "stay_id": stay_id,
        "clinical_facts": organized_facts
    }


@app.get("/stays/{stay_id}/evidence/{code}")
async def get_code_evidence(
    stay_id: str,
    code: str,
    db: Session = Depends(get_db),
):
    """
    Récupère les preuves pour un code spécifique.

    Permet la navigation preuves → texte source.

    Exigences : 10.2, 10.3
    """
    # Récupérer le séjour
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")

    # Récupérer le code
    code_db = (
        db.query(CodeDB)
        .filter(CodeDB.stay_id == stay.id, CodeDB.code == code)
        .first()
    )

    if not code_db:
        raise HTTPException(
            status_code=404, detail=f"Code {code} not found for stay {stay_id}"
        )

    # Récupérer les preuves
    evidences = []
    for ev_db in code_db.evidences:
        evidences.append(
            {
                "document_id": ev_db.document.document_id,
                "document_type": ev_db.document.document_type,
                "span": {"start": ev_db.span_start, "end": ev_db.span_end},
                "text": ev_db.text,
                "context": ev_db.context,
                # Lien vers le document source
                "document_link": f"/documents/{ev_db.document.document_id}",
            }
        )

    return {
        "stay_id": stay_id,
        "code": code,
        "label": code_db.label,
        "evidences": evidences,
    }


@app.get("/documents/{document_id}")
async def get_document(
    document_id: str,
    db: Session = Depends(get_db),
):
    """
    Récupère le contenu d'un document.

    Permet la navigation vers le texte source.

    Exigences : 10.2, 10.3
    """
    from pipeline_mco_pmsi.database.models import ClinicalDocumentDB

    # Récupérer le document
    document = (
        db.query(ClinicalDocumentDB)
        .filter(ClinicalDocumentDB.document_id == document_id)
        .first()
    )

    if not document:
        raise HTTPException(status_code=404, detail=f"Document {document_id} not found")

    return {
        "document_id": document.document_id,
        "document_type": document.document_type,
        "content": document.content,
        "creation_date": document.creation_date.isoformat(),
        "author": document.author,
        "priority": document.priority,
    }


@app.post("/stays/{stay_id}/process")
async def process_stay(
    stay_id: str,
    specialty: str = "chirurgie",
    db: Session = Depends(get_db),
):
    """
    Traite un séjour existant avec ses documents déjà en base.
    
    Cette route permet de lancer le traitement d'un séjour depuis l'interface web.
    """
    from pipeline_mco_pmsi.database.models import ClinicalDocumentDB
    from pipeline_mco_pmsi.models.clinical import ClinicalDocument
    from pipeline_mco_pmsi.models.metadata import StayMetadata
    from pipeline_mco_pmsi.pipeline import Pipeline
    from pipeline_mco_pmsi.rag.rag_engine import RAGEngine
    from pipeline_mco_pmsi.rag.referentiels_manager import ReferentielsManager
    
    # Vérifier que le séjour existe
    stay = db.query(StayDB).filter(StayDB.stay_id == stay_id).first()
    if not stay:
        raise HTTPException(status_code=404, detail=f"Stay {stay_id} not found")
    
    # Charger les documents
    documents = []
    for doc_db in stay.documents:
        doc = ClinicalDocument(
            document_id=doc_db.document_id,
            document_type=doc_db.document_type,
            content=doc_db.content,
            creation_date=doc_db.creation_date,
            author=doc_db.author or "Inconnu",
            priority=doc_db.priority
        )
        documents.append(doc)
    
    if not documents:
        raise HTTPException(
            status_code=400, 
            detail=f"No documents found for stay {stay_id}. Please upload documents first."
        )
    
    # Créer le pipeline
    referentiels_manager = ReferentielsManager(data_dir=Path("data/referentiels"))
    rag_engine = RAGEngine(referentiels_manager=referentiels_manager)
    pipeline = Pipeline(db_session=db, rag_engine=rag_engine)
    
    # Créer les métadonnées
    stay_metadata = StayMetadata(
        stay_id=stay.stay_id,
        admission_date=stay.admission_date,
        discharge_date=stay.discharge_date,
        specialty=specialty
    )
    
    # Traiter
    try:
        result = pipeline.process_stay(documents=documents, stay_metadata=stay_metadata)
        
        return {
            "success": result.success,
            "stay_id": stay_id,
            "message": "Traitement terminé avec succès" if result.success else "Traitement échoué",
            "has_dp": result.coding_proposal.dp is not None if result.coding_proposal else False,
            "num_das": len(result.coding_proposal.das) if result.coding_proposal else 0,
            "num_ccam": len(result.coding_proposal.ccam) if result.coding_proposal else 0,
            "num_questions": len(result.questions),
            "num_validation_issues": len(result.validation_issues)
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Error processing stay: {str(e)}")


# Point d'entrée pour lancer l'application
if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)
