from contextlib import asynccontextmanager
import json
import os
from fastapi import Depends, FastAPI, File, HTTPException, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from sqlalchemy import inspect, text
from .database import Base, engine, get_db
from . import models, schemas
from .auth import (
    AUTH_COOKIE_NAME,
    create_access_token,
    get_current_user,
    hash_password,
    require_roles,
    seed_admin_user,
    verify_password,
)
from .curriculum import QUESTIONS
from .program_content import (
    decode_asset_token,
    get_content_status,
    get_lesson,
    list_lessons_for_grade,
    normalize_grade,
    render_svg_section,
)
from .services import (
    build_llm_reply,
    build_lesson_reply,
    ensure_student_mastery,
    evaluate_answer,
    list_tts_profiles,
    pick_next_skill,
    seed_skills,
    synthesize_speech,
    transcribe_audio,
)


@asynccontextmanager
async def lifespan(app: FastAPI):
    Base.metadata.create_all(bind=engine)
    ensure_lightweight_migrations()
    db = next(get_db())
    try:
        seed_skills(db)
        seed_admin_user(db)
    finally:
        db.close()
    yield


app = FastAPI(title="Professeur Virtuel API", version="0.1.0", lifespan=lifespan)

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://prof.open-squared.tech",
        "http://localhost:3000",
        "http://localhost:3001",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


def ensure_lightweight_migrations() -> None:
    inspector = inspect(engine)
    if "student_program_assignments" in inspector.get_table_names():
        columns = {column["name"] for column in inspector.get_columns("student_program_assignments")}
        with engine.begin() as connection:
            if "start_asset_index" not in columns:
                connection.execute(
                    text("ALTER TABLE student_program_assignments ADD COLUMN start_asset_index INTEGER DEFAULT 0")
                )
            if "start_section_index" not in columns:
                connection.execute(
                    text("ALTER TABLE student_program_assignments ADD COLUMN start_section_index INTEGER DEFAULT 0")
                )


def ensure_student_access(current_user: models.User, student_id: int) -> None:
    if current_user.role in {"teacher", "maintenance"}:
        return
    if current_user.role == "student" and current_user.student_id == student_id:
        return
    raise HTTPException(status_code=403, detail="Droits insuffisants pour cet élève")


def build_student_program_response(student: models.Student, assignment: models.StudentProgramAssignment | None):
    lesson = get_lesson(assignment.lesson_id) if assignment else None
    return schemas.StudentProgramResponse(
        student=student,
        lesson=lesson,
        start_asset_index=assignment.start_asset_index if assignment else 0,
        start_section_index=assignment.start_section_index if assignment else 0,
    )


def clamp_lesson_position(lesson: dict, asset_index: int, section_index: int) -> tuple[int, int]:
    assets = lesson.get("assets") or []
    if not assets:
        return 0, 0
    safe_asset_index = min(max(asset_index, 0), len(assets) - 1)
    sections = assets[safe_asset_index].get("sections") or []
    safe_section_index = min(max(section_index, 0), max(len(sections) - 1, 0))
    return safe_asset_index, safe_section_index


def upsert_card_progress(db: Session, student_id: int, state: schemas.LessonCardState) -> models.LessonCardProgress:
    row = (
        db.query(models.LessonCardProgress)
        .filter_by(
            student_id=student_id,
            lesson_id=state.lesson_id,
            asset_path=state.asset_path,
            section_key=state.section_key,
        )
        .first()
    )
    if not row:
        row = models.LessonCardProgress(
            student_id=student_id,
            lesson_id=state.lesson_id,
            asset_path=state.asset_path,
            asset_title=state.asset_title,
            section_key=state.section_key,
            section_title=state.section_title,
        )
    row.asset_title = state.asset_title
    row.section_title = state.section_title
    row.comprehension_score = max(row.comprehension_score or 0, state.comprehension_score)
    row.attempts = max(row.attempts or 0, state.attempts)
    if state.validated and row.validated_at is None:
        from datetime import datetime

        row.validated_at = datetime.utcnow()
    db.add(row)
    return row


def update_next_program_start_from_progress(db: Session, student_id: int, lesson_id: str) -> None:
    assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student_id).first()
    if not assignment or assignment.lesson_id != lesson_id:
        return
    lesson = get_lesson(lesson_id)
    if not lesson:
        return
    validated_keys = {
        row.section_key
        for row in db.query(models.LessonCardProgress)
        .filter_by(student_id=student_id, lesson_id=lesson_id)
        .all()
        if row.validated_at is not None
    }
    assets = lesson.get("assets") or []
    for asset_index, asset in enumerate(assets):
        sections = asset.get("sections") or []
        if not sections:
            continue
        for section_index, section in enumerate(sections):
            section_key = section.get("context_key") or f"{asset.get('path', 'asset')}:{section.get('index', 0)}"
            if section_key not in validated_keys:
                assignment.start_asset_index = asset_index
                assignment.start_section_index = section_index
                db.add(assignment)
                return
    last_asset_index = max(len(assets) - 1, 0)
    last_sections = assets[last_asset_index].get("sections") if assets else []
    assignment.start_asset_index = last_asset_index
    assignment.start_section_index = max(len(last_sections or []) - 1, 0)
    db.add(assignment)

@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/auth/login", response_model=schemas.LoginResponse)
def login(payload: schemas.LoginRequest, response: Response, db: Session = Depends(get_db)):
    user = db.query(models.User).filter_by(username=payload.username).first()
    if not user or not verify_password(payload.password, user.password_hash):
        raise HTTPException(status_code=401, detail="Identifiants invalides")

    token = create_access_token(user)
    response.set_cookie(
        key=AUTH_COOKIE_NAME,
        value=token,
        httponly=True,
        secure=os.getenv("AUTH_COOKIE_SECURE", "false").lower() == "true",
        samesite=os.getenv("AUTH_COOKIE_SAMESITE", "lax"),
        max_age=int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "720")) * 60,
        path="/",
    )
    return schemas.LoginResponse(access_token=token, user=user)


@app.get("/auth/me", response_model=schemas.UserRead)
def auth_me(current_user: models.User = Depends(get_current_user)):
    return current_user


@app.post("/auth/logout")
def logout(response: Response):
    response.delete_cookie(key=AUTH_COOKIE_NAME, path="/")
    return {"status": "ok"}


@app.get("/students", response_model=list[schemas.StudentRead])
def list_students(
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    return db.query(models.Student).order_by(models.Student.id.asc()).all()


@app.post("/students", response_model=schemas.StudentRead)
def create_student(
    payload: schemas.StudentCreate,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    student = models.Student(**payload.model_dump())
    db.add(student)
    db.commit()
    db.refresh(student)
    ensure_student_mastery(db, student)
    return student


@app.post("/admin/student-accounts", response_model=schemas.StudentAccountResponse)
def create_student_account(
    payload: schemas.StudentAccountCreate,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    username = payload.username.strip()
    first_name = payload.first_name.strip()
    if not username or not first_name:
        raise HTTPException(status_code=400, detail="Prénom et identifiant obligatoires")

    existing_user = db.query(models.User).filter_by(username=username).first()
    if existing_user:
        raise HTTPException(status_code=409, detail="Identifiant deja utilise")

    student = models.Student(
        first_name=first_name,
        age=payload.age,
        grade=payload.grade,
    )
    db.add(student)
    db.commit()
    db.refresh(student)
    ensure_student_mastery(db, student)

    user = models.User(
        username=username,
        password_hash=hash_password(payload.password),
        role="student",
        student_id=student.id,
    )
    db.add(user)
    db.commit()
    db.refresh(user)
    return schemas.StudentAccountResponse(student=student, user=user)


@app.get("/students/{student_id}/messages", response_model=list[schemas.MessageRead])
def list_student_messages(
    student_id: int,
    limit: int = 80,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, student_id)
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")

    safe_limit = min(max(limit, 1), 200)
    rows = (
        db.query(models.Message)
        .filter_by(student_id=student_id)
        .order_by(models.Message.created_at.desc(), models.Message.id.desc())
        .limit(safe_limit)
        .all()
    )
    return list(reversed(rows))


@app.get("/program/lessons", response_model=schemas.ProgramLessonListResponse)
def list_program_lessons(
    grade: str,
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    return schemas.ProgramLessonListResponse(lessons=list_lessons_for_grade(grade))


@app.get("/program/status")
def get_program_status(
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    return get_content_status()


@app.get("/program/assets/{asset_token}")
def get_program_asset(
    asset_token: str,
    current_user: models.User = Depends(get_current_user),
):
    try:
        path = decode_asset_token(asset_token)
    except Exception as exc:
        raise HTTPException(status_code=400, detail="Ressource invalide") from exc
    if not path.exists() or path.suffix.lower() != ".svg":
        raise HTTPException(status_code=404, detail="Fiche introuvable")
    return FileResponse(path, media_type="image/svg+xml")


@app.get("/program/assets/{asset_token}/sections/{section_index}")
def get_program_asset_section(
    asset_token: str,
    section_index: int,
    current_user: models.User = Depends(get_current_user),
):
    try:
        path = decode_asset_token(asset_token)
        svg = render_svg_section(path, section_index)
    except IndexError as exc:
        raise HTTPException(status_code=404, detail="Étape introuvable") from exc
    except Exception as exc:
        raise HTTPException(status_code=400, detail="Ressource invalide") from exc
    return Response(content=svg, media_type="image/svg+xml")


@app.get("/students/{student_id}/program", response_model=schemas.StudentProgramResponse)
def get_student_program(
    student_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, student_id)
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")
    assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student_id).first()
    return build_student_program_response(student, assignment)


@app.put("/admin/students/{student_id}/program", response_model=schemas.StudentProgramResponse)
def assign_student_program(
    student_id: int,
    payload: schemas.StudentProgramAssignRequest,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")

    lesson = get_lesson(payload.lesson_id)
    if not lesson:
        raise HTTPException(status_code=404, detail="Leçon introuvable")
    if lesson["grade"] != normalize_grade(student.grade):
        raise HTTPException(status_code=400, detail="Cette leçon ne correspond pas au niveau de l’élève")

    assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student_id).first()
    if not assignment:
        assignment = models.StudentProgramAssignment(student_id=student_id)
    assignment.lesson_id = lesson["id"]
    assignment.title = lesson["title"]
    assignment.subject = lesson["subject"]
    assignment.grade = lesson["grade"]
    db.add(assignment)
    db.commit()
    db.refresh(assignment)
    return build_student_program_response(student, assignment)


@app.put("/admin/students/{student_id}/program/start", response_model=schemas.StudentProgramResponse)
def update_student_program_start(
    student_id: int,
    payload: schemas.StudentProgramStartRequest,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")
    assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student_id).first()
    if not assignment:
        raise HTTPException(status_code=404, detail="Aucune leçon active")
    lesson = get_lesson(assignment.lesson_id)
    if not lesson:
        raise HTTPException(status_code=404, detail="Leçon introuvable")
    asset_index, section_index = clamp_lesson_position(lesson, payload.asset_index, payload.section_index)
    assignment.start_asset_index = asset_index
    assignment.start_section_index = section_index
    db.add(assignment)
    db.commit()
    db.refresh(assignment)
    return build_student_program_response(student, assignment)


@app.get("/students/{student_id}/card-progress", response_model=list[schemas.LessonCardProgressRead])
def list_card_progress(
    student_id: int,
    lesson_id: str | None = None,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, student_id)
    query = db.query(models.LessonCardProgress).filter_by(student_id=student_id)
    if lesson_id:
        query = query.filter_by(lesson_id=lesson_id)
    return query.order_by(models.LessonCardProgress.updated_at.desc()).all()


@app.post("/session/end", response_model=schemas.LessonSessionLogRead)
def end_lesson_session(
    payload: schemas.LessonSessionEndRequest,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, payload.student_id)
    student = db.query(models.Student).filter_by(id=payload.student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")

    for state in payload.card_states:
        upsert_card_progress(db, student.id, state)
    db.flush()
    update_next_program_start_from_progress(db, student.id, payload.lesson_id)

    conversation_json = json.dumps(
        payload.conversation,
        ensure_ascii=False,
    )
    card_results_json = json.dumps(
        [state.model_dump(mode="json") for state in payload.card_states],
        ensure_ascii=False,
    )
    log = models.LessonSessionLog(
        student_id=student.id,
        lesson_id=payload.lesson_id,
        lesson_title=payload.lesson_title,
        conversation=conversation_json,
        card_results=card_results_json,
    )
    db.add(log)
    db.commit()
    db.refresh(log)
    return log


@app.get("/admin/students/{student_id}/session-logs", response_model=list[schemas.LessonSessionLogRead])
def list_session_logs(
    student_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    return (
        db.query(models.LessonSessionLog)
        .filter_by(student_id=student_id)
        .order_by(models.LessonSessionLog.ended_at.desc(), models.LessonSessionLog.id.desc())
        .all()
    )


@app.delete("/admin/session-logs/{log_id}")
def delete_session_log(
    log_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(require_roles("teacher", "maintenance")),
):
    log = db.query(models.LessonSessionLog).filter_by(id=log_id).first()
    if not log:
        raise HTTPException(status_code=404, detail="Log introuvable")
    db.delete(log)
    db.commit()
    return {"status": "ok"}


@app.post("/session/start", response_model=schemas.ChatResponse)
def start_session(
    student_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, student_id)
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")

    ensure_student_mastery(db, student)
    message = (
        f"Bonjour {student.first_name} ! Je suis Professeur TOP, ton professeur virtuel. "
        "Aujourd'hui, on va apprendre pas à pas et faire un petit test pour voir ce que tu maîtrises déjà."
    )
    assignment = db.query(models.StudentProgramAssignment).filter_by(student_id=student.id).first()
    assigned_lesson = get_lesson(assignment.lesson_id) if assignment else None
    if assigned_lesson:
        asset_index, section_index = clamp_lesson_position(
            assigned_lesson,
            assignment.start_asset_index if assignment else 0,
            assignment.start_section_index if assignment else 0,
        )
        first_asset = (assigned_lesson.get("assets") or [None])[asset_index]
        first_sections = first_asset.get("sections") or [] if first_asset else []
        first_section = first_sections[section_index] if first_sections else None
        section_label = first_section["title"] if first_section else first_asset["title"] if first_asset else assigned_lesson["title"]
        lesson_context = (
            f"Leçon courante: {assigned_lesson['title']}\n"
            f"Fiche courante: {first_asset['title'] if first_asset else 'non précisée'}\n"
            f"Card courante: {section_label}\n"
            f"Clé de contexte: {first_section.get('context_key') if first_section else 'non précisée'}\n"
            "Avancement affiché: début de leçon\n"
            "Règle: commence directement sur cette card. Ne fais pas de reprise de la dernière séance.\n"
        )
        if first_section and first_section.get("context"):
            lesson_context += f"\nTexte pédagogique de la card courante:\n{first_section['context'].strip()}"
        lesson_reply = build_lesson_reply(
            db,
            student.id,
            "La séance démarre. Présente la card courante et pose une question de compréhension.",
            lesson_context,
        )
        message = lesson_reply["reply"]
    previous_messages_count = db.query(models.Message).filter_by(student_id=student.id).count()
    if previous_messages_count and not assigned_lesson:
        prompt = (
            "L’élève revient pour une nouvelle séance. Fais une courte reprise: "
            "explique où on s’était arrêté la dernière fois à partir de l’historique, "
            "puis annonce clairement ce qu'on va faire aujourd'hui. Termine par une "
            "première consigne simple."
        )
        message = build_llm_reply(db, student.id, prompt)
    db.add(models.Message(student_id=student.id, role="assistant", content=message))
    db.commit()
    return schemas.ChatResponse(reply=message)


@app.post("/chat", response_model=schemas.ChatResponse)
def chat(
    payload: schemas.ChatRequest,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, payload.student_id)
    student = db.query(models.Student).filter_by(id=payload.student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")

    db.add(models.Message(student_id=student.id, role="user", content=payload.message))
    db.commit()

    lesson_context = ""
    if payload.lesson_title or payload.section_title or payload.asset_title:
        lesson_context = (
            f"Leçon courante: {payload.lesson_title or 'non précisée'}\n"
            f"Fiche courante: {payload.asset_title or 'non précisée'}\n"
            f"Card courante: {payload.section_title or 'non précisée'}\n"
            f"Clé de contexte: {payload.section_context_key or 'non précisée'}\n"
            f"Avancement affiché: {payload.step_percent if payload.step_percent is not None else 'non précisé'}%\n"
            "Règle: réponds uniquement à propos de cette card et vérifie la compréhension de l'élève.\n"
        )
        if payload.section_context:
            lesson_context += f"\nTexte pédagogique de la card courante:\n{payload.section_context.strip()}"

    if lesson_context:
        lesson_reply = build_lesson_reply(db, payload.student_id, payload.message, lesson_context)
        reply = lesson_reply["reply"]
        advance_lesson_step = lesson_reply["advance_lesson_step"]
    else:
        reply = build_llm_reply(db, payload.student_id, payload.message)
        advance_lesson_step = False

    db.add(models.Message(student_id=student.id, role="assistant", content=reply))
    db.commit()
    return schemas.ChatResponse(reply=reply, advance_lesson_step=advance_lesson_step)


@app.post("/transcribe")
async def transcribe(
    file: UploadFile = File(...),
    current_user: models.User = Depends(get_current_user),
):
    if not file.filename:
        raise HTTPException(status_code=400, detail="Fichier audio manquant")

    audio_bytes = await file.read()
    if not audio_bytes:
        raise HTTPException(status_code=400, detail="Fichier audio vide")

    try:
        text = transcribe_audio(file.filename, audio_bytes, file.content_type)
    except Exception as exc:
        raise HTTPException(status_code=502, detail=f"Erreur de transcription: {exc}") from exc

    return {"text": text}


@app.get("/tts/profiles", response_model=schemas.TTSProfilesResponse)
def get_tts_profiles():
    return schemas.TTSProfilesResponse(profiles=list_tts_profiles())


@app.post("/tts")
def text_to_speech(
    payload: schemas.TTSRequest,
    current_user: models.User = Depends(get_current_user),
):
    try:
        audio_bytes = synthesize_speech(payload.text, payload.profile_id)
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc)) from exc
    except Exception as exc:
        raise HTTPException(status_code=502, detail=f"Erreur de synthèse vocale: {exc}") from exc

    return Response(content=audio_bytes, media_type="audio/mpeg")


@app.get("/progress/{student_id}", response_model=schemas.ProgressResponse)
def get_progress(
    student_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, student_id)
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")

    rows = (
        db.query(models.StudentSkillMastery, models.Skill)
        .join(models.Skill, models.Skill.id == models.StudentSkillMastery.skill_id)
        .filter(models.StudentSkillMastery.student_id == student_id)
        .order_by(models.Skill.subject.asc(), models.Skill.label.asc())
        .all()
    )
    progress = [
        schemas.SkillProgress(
            code=skill.code,
            subject=skill.subject,
            label=skill.label,
            mastery_score=mastery.mastery_score,
            confidence=mastery.confidence,
            evidence_count=mastery.evidence_count,
        )
        for mastery, skill in rows
    ]
    return schemas.ProgressResponse(student=student, progress=progress)


@app.get("/assessment/next/{student_id}", response_model=schemas.AssessmentQuestionResponse)
def next_assessment(
    student_id: int,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, student_id)
    student = db.query(models.Student).filter_by(id=student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")
    skill = pick_next_skill(db, student_id)
    question = QUESTIONS[skill.code]["question"]
    return schemas.AssessmentQuestionResponse(skill_code=skill.code, skill_label=skill.label, question=question)


@app.post("/assessment/answer", response_model=schemas.AssessmentAnswerResponse)
def answer_assessment(
    payload: schemas.AssessmentAnswerRequest,
    db: Session = Depends(get_db),
    current_user: models.User = Depends(get_current_user),
):
    ensure_student_access(current_user, payload.student_id)
    student = db.query(models.Student).filter_by(id=payload.student_id).first()
    if not student:
        raise HTTPException(status_code=404, detail="Élève introuvable")
    if payload.skill_code not in QUESTIONS:
        raise HTTPException(status_code=400, detail="Compétence inconnue")

    correct, feedback, mastery_score = evaluate_answer(
        db, payload.student_id, payload.skill_code, payload.answer
    )
    db.add(
        models.Message(
            student_id=student.id,
            role="user",
            content=f"Reponse au mini-test ({payload.skill_code}): {payload.answer}",
        )
    )
    db.add(models.Message(student_id=student.id, role="assistant", content=feedback))
    db.commit()
    return schemas.AssessmentAnswerResponse(
        correct=correct,
        feedback=feedback,
        mastery_score=mastery_score,
    )
