"""
Assessment Report Analysis — FastAPI Layer
===========================================

Enterprise-grade HTTP API for the LMS Assessment Skill Intelligence feature.
Provides a two-phase workflow:

  Phase 1 — TRAIN:  POST /train/{client_id}
      Accepts the organisation's data, queues background LLM analysis,
      and persists the results isolated per client.

  Phase 2 — REPORT: GET /report/{client_id}
      Retrieves the pre-computed analysis for a specific client instantly.

All data is client-isolated — no cross-contamination between organisations.

Architecture:
  PHP Controller  →  THIS FastAPI FILE  →  Processor  →  Ollama LLM
                                                ↕
                                       Client Data Store
"""

import os
import logging
from typing import Any, Dict, List, Optional

from fastapi import FastAPI, HTTPException, Path, status, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field

# ---------------------------------------------------------------------------
# Import processor module
# ---------------------------------------------------------------------------
try:
    from assessment_report_analysis_processor import (
        start_training_meta,
        run_training_pipeline,
        get_training_status,
        get_analysis_result,
        list_all_clients,
        delete_client_data,
        check_ollama_health,
        start_client_session,
        append_batch_data,
        finalize_training,
        TrainingStatus,
        OLLAMA_MODEL,
        OLLAMA_BASE_URL,
    )
except ImportError:
    import sys
    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    from assessment_report_analysis_processor import (
        start_training_meta,
        run_training_pipeline,
        get_training_status,
        get_analysis_result,
        list_all_clients,
        delete_client_data,
        check_ollama_health,
        start_client_session,
        append_batch_data,
        finalize_training,
        TrainingStatus,
        OLLAMA_MODEL,
        OLLAMA_BASE_URL,
    )

# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)
logger = logging.getLogger("assessment_report_api")


# ===========================================================================
# SECTION 1 — INPUT Pydantic Models
# ===========================================================================

# ─── Question Bank ──────────────────────────────────────────────────────────

class QuestionBankItem(BaseModel):
    id:             str           = Field(...,  description="Unique encrypted question identifier.")
    questionText:   str           = Field(...,  description="Full question content (may contain HTML).")
    questionType:   int           = Field(...,  ge=1, le=11, description="Question format type (1–11).")
    explanation:    Optional[str] = Field("",   description="Answer rationale/explanation.")
    complexity:     int           = Field(...,  description="Difficulty/cognitive load score.")
    marks:          int           = Field(...,  description="Maximum marks.")
    correctAnswer:  Optional[str] = Field("",   description="Correct answer value.")
    questionOption: Optional[str] = Field("",   description="Comma-separated answer options.")
    categoryName:   Optional[str] = Field("",   description="Category/module name of the question for classification.")
    class Config:
        extra = "ignore"


class QuestionBankPayload(BaseModel):
    status:        bool                   = Field(...)
    message:       str                    = Field(...)
    data:          List[QuestionBankItem]  = Field(...)
    TotalQuestion: Optional[int]          = Field(None)
    class Config:
        extra = "ignore"


# ─── Assessment Summary ──────────────────────────────────────────────────────

class UserAssignDataItem(BaseModel):
    name:  str = Field(...)
    value: int = Field(...)
    class Config:
        extra = "ignore"


class LearningData(BaseModel):
    learningAccuracyTotalRight: int   = Field(...)
    learningAccuracyTotalWrong: int   = Field(...)
    learningAccuracyRightPer:   float = Field(...)
    learningAccuracyWrongPer:   float = Field(...)
    class Config:
        extra = "ignore"


class AssessmentSummaryItem(BaseModel):
    id:             int                      = Field(...)
    name:           str                      = Field(...)
    enrolled:       int                      = Field(...)
    completed:      int                      = Field(...)
    notStarted:     int                      = Field(...)
    userAssignData: List[UserAssignDataItem]  = Field(...)
    learningData:   LearningData             = Field(...)
    class Config:
        extra = "ignore"


class AssessmentSummaryPayload(BaseModel):
    status:          bool                        = Field(...)
    message:         str                         = Field(...)
    data:            List[AssessmentSummaryItem]  = Field(...)
    assessmentCount: Optional[int]               = Field(None)
    class Config:
        extra = "ignore"


# ─── Completed User Attempt Records ─────────────────────────────────────────

class CompletedUserRecord(BaseModel):
    id:                 int           = Field(...)
    assessmentId:       int           = Field(...)
    userId:             int           = Field(...)
    userName:           Optional[str] = Field("")
    managerId:          Optional[int] = Field(None, description="Manager ID for hierarchical reporting.")
    managerName:        Optional[str] = Field("", description="Manager name for display purposes.")
    quizName:           Optional[str] = Field("")
    totalMarks:         int           = Field(...)
    obtainedMarks:      int           = Field(...)
    totalObtPercentage: float         = Field(...)
    pass_:              Optional[bool] = Field(None, alias="pass")
    attemptNumber:      Optional[Any] = Field(None)
    isSection:          Optional[int] = Field(0)
    isSubmitType:       Optional[Any] = Field(None, description="Submission mode (may be string or int from PHP).")
    grade:              Optional[str] = Field("")
    class Config:
        extra = "ignore"
        populate_by_name = True


class CompletedUsersPayload(BaseModel):
    status:          bool                       = Field(...)
    message:         str                        = Field(...)
    data:            List[CompletedUserRecord]   = Field(...)
    assessmentName:  Optional[str]              = Field(None)
    assessmentCount: Optional[int]              = Field(None)
    class Config:
        extra = "ignore"


# ─── Individual User Attempt Detail ─────────────────────────────────────────

class QuestionOption(BaseModel):
    correct_answer_option:  bool          = Field(...)
    user_answer:            bool          = Field(...)
    option_id:              int           = Field(...)
    question_option:        str           = Field(...)
    question_option_image:  Optional[str] = Field("")
    class Config:
        extra = "ignore"


class AttemptQuestionDetail(BaseModel):
    question_id:                int                  = Field(...)
    question_name:              str                  = Field(...)
    question_type:              int                  = Field(...)
    question_explanation:       Optional[str]        = Field("")
    is_attempted:               Optional[bool]       = Field(True)
    correct_answer:             Optional[str]        = Field("")
    user_answer:                Optional[str]        = Field("")
    quesType:                   Optional[str]        = Field(None)
    isGrade:                    Optional[int]        = Field(0)
    gradeMark:                  Optional[float]      = Field(0)
    total_marks:                int                  = Field(...)
    obtained_marks:             int                  = Field(...)
    question_option:            List[QuestionOption]  = Field(...)
    user_question_option:       Optional[str]        = Field("")
    user_question_option_image: Optional[str]        = Field("")
    marks_range:                Optional[List[Any]]  = Field([])
    class Config:
        extra = "ignore"


class UserAttemptDetailPayload(BaseModel):
    status:               Optional[bool]               = Field(None)
    message:              Optional[str]                = Field(None)
    userId:               Optional[int]                = Field(None, description="Unique user identifier.")
    userName:             Optional[str]                = Field("")
    managerId:            Optional[int]                = Field(None, description="Manager ID for hierarchical reporting.")
    managerName:          Optional[str]                = Field("", description="Manager name for display purposes.")
    totalScore:           Optional[int]                = Field(None)
    obtainScore:          Optional[int]                = Field(None)
    userPercentage:       Optional[float]              = Field(None)
    passingMarks:         Optional[float]              = Field(None)
    assessmentId:         Optional[int]                = Field(None)
    assessmentName:       Optional[str]                = Field("")
    contentId:            Optional[int]                = Field(None)
    quizName:             Optional[str]                = Field("")
    isProctoring:         Optional[int]                = Field(None)
    isGamification:       Optional[int]                = Field(None)
    totalTabChanged:      Optional[int]                = Field(0)
    totalFaceRemoved:     Optional[int]                = Field(0)
    quizStartTime:        Optional[str]                = Field(None)
    quizEndTime:          Optional[str]                = Field(None)
    quizDuration:         Optional[int]                = Field(None)
    galleryDetailId:      Optional[int]                = Field(None)
    proctoringConfidence: Optional[float]              = Field(None)
    data:                 List[AttemptQuestionDetail]  = Field(...)
    class Config:
        extra = "ignore"


# ===========================================================================
# SECTION 2 — REQUEST / RESPONSE Models
# ===========================================================================

class TrainRequest(BaseModel):
    """
    Standardised payload for initiating analysis training.
    """
    question_bank:        List[QuestionBankItem]         = Field(..., description="Organisation question bank (flat list).")
    assessment_summary:   List[AssessmentSummaryItem]     = Field(default_factory=list, description="Assessment catalogue (flat list).")
    completed_users_data: List[CompletedUserRecord]       = Field(default_factory=list, description="Completed user attempt records (flat list).")
    user_attempt_details: List[UserAttemptDetailPayload]  = Field(..., description="Granular per-user attempt details.")
    manager_mapping:      Optional[Dict[str, List[int]]] = Field(None, description="Optional mapping of Manager ID to list of User IDs.")
    class Config:
        extra = "ignore"


class BatchTrainRequest(BaseModel):
    """
    Payload for a single training batch.
    Contains a chunk of questions and their related user attempt details.
    """
    question_bank:        List[QuestionBankItem]         = Field(..., description="Batch of questions to train.")
    user_attempt_details: List[UserAttemptDetailPayload]  = Field(default_factory=list, description="User attempt details related to this batch's questions.")
    class Config:
        extra = "ignore"


# ─── Role-Based Response Models ───────────────────────────────────────────

# ── Per-Question Deep Analytics ─────────────────────────────────────────

class QuestionUserAttempt(BaseModel):
    """Individual user's interaction with a specific question."""
    user_id: str
    user_name: str = ""
    is_correct: bool = False
    obtained_marks: int = 0
    total_marks: int = 0
    assessment_id: Optional[int] = None
    assessment_name: Optional[str] = ""
    total_attempts_by_user: int = 1
    class Config:
        extra = "allow"


class QuestionAssessmentAppearance(BaseModel):
    """Tracks which assessments a question appears in."""
    assessment_id: Any = None
    assessment_name: str = ""
    attempts_in_assessment: int = 0
    correct_in_assessment: int = 0
    accuracy_in_assessment: float = 0.0
    class Config:
        extra = "allow"


class QuestionReport(BaseModel):
    """
    Professional Question Intelligence Mapping — V3.
    Covers every question in the bank with full analytics.
    """
    question_id: str
    question_text_preview: str = ""
    category: str = "Uncategorized"
    subject_domain: str = "General Knowledge"
    primary_skill: str = "General Reasoning"
    skill_tier: str = "Beginner"
    question_type: int = 0
    question_format_descriptor: str = "Unknown"
    complexity_score: int = 0
    max_marks: int = 0
    # ── Attempt analytics ──────────────────────────────────────────────
    total_attempts: int = 0
    correct_count: int = 0
    wrong_count: int = 0
    unattempted_count: int = 0
    average_accuracy_across_users: float = 0.0
    average_marks_obtained: float = 0.0
    # ── User linkage ────────────────────────────────────────────────────
    user_count: int = 0
    users_attempted: List[QuestionUserAttempt] = []
    assessments_appeared_in: List[QuestionAssessmentAppearance] = []
    # ── Intelligence ────────────────────────────────────────────────────
    difficulty_rating: str = "Not Attempted"
    discrimination_index: str = "N/A"
    status: str = "Unused"
    classification_source: str = "unknown"
    class Config:
        extra = "allow"


# ── Question Bank Summary ────────────────────────────────────────────────

class QuestionBankSummary(BaseModel):
    """Aggregate statistics across the entire question bank."""
    total_questions: int = 0
    total_active: int = 0
    total_unused: int = 0
    category_distribution: Dict[str, int] = {}
    domain_distribution: Dict[str, int] = {}
    skill_distribution: Dict[str, int] = {}
    skill_tier_distribution: Dict[str, int] = {}
    difficulty_distribution: Dict[str, int] = {}
    question_type_distribution: Dict[str, int] = {}
    classification_source_breakdown: Dict[str, int] = {}
    average_accuracy_of_attempted_questions: float = 0.0
    average_complexity: float = 0.0
    class Config:
        extra = "allow"


# ── User Question Detail (inside user reports) ──────────────────────────

class UserQuestionDetail(BaseModel):
    """Per-question detail attached to an individual user report."""
    question_id: str
    text_preview: str = ""
    category: str = "Uncategorized"
    domain: str = "General Knowledge"
    primary_skill: str = "General Reasoning"
    skill_level: str = "Beginner"
    is_correct: bool = False
    obtained_marks: int = 0
    total_marks: int = 0
    assessment_id: Optional[int] = None
    assessment_name: Optional[str] = ""


# ── Core Report Models ──────────────────────────────────────────────────

class AdminSummary(BaseModel):
    """Admin-level summary counters."""
    total_questions_in_bank: int = 0
    total_active_questions: int = 0
    total_unused_questions: int = 0
    total_users: int = 0
    total_assessments: int = 0
    average_accuracy: float = 0.0
    class Config:
        extra = "allow"


class SkillInsights(BaseModel):
    top_skills: List[str] = []
    weak_skills: List[str] = []
    skill_accuracy_details: List[Dict[str, Any]] = []
    class Config:
        extra = "allow"


class AdminReport(BaseModel):
    """
    Full admin-level analytics report.
    question_skill_map contains EVERY question in the bank with complete analytics.
    """
    summary: AdminSummary
    skill_insights: SkillInsights
    question_bank_summary: QuestionBankSummary = Field(default_factory=QuestionBankSummary)
    question_skill_map: List[QuestionReport] = []
    assessment_health: List[Dict[str, Any]] = []
    integrity_flags: List[Dict[str, Any]] = []
    key_insights: List[str] = []
    actions: List[str] = []
    behavior_patterns: List[str] = []
    high_failure_questions: List[str] = []
    chart_data: Dict[str, Any] = Field(default_factory=dict, description="Advanced visualizations data for admin dashboard.")
    class Config:
        extra = "allow"


class ManagerReport(BaseModel):
    manager_id: str
    manager_name: str = ""
    team_size: int = 0
    team_accuracy: float = 0.0
    top_performers: List[str] = []
    low_performers: List[str] = []
    skill_gaps: List[str] = []
    skill_accuracy: Dict[str, float] = {}
    recommendations: List[str] = []
    team_members: List[Dict[str, Any]] = Field(default_factory=list, description="List of team members.")
    team_assessments_attempted: List[Dict[str, Any]] = Field(default_factory=list, description="Assessments taken by the team.")
    chart_data: Dict[str, Any] = Field(default_factory=dict, description="Advanced visualizations data for manager dashboard.")
    class Config:
        extra = "allow"


class UserReport(BaseModel):
    user_id: str
    user_name: str = ""
    accuracy: float = 0.0
    overall_score_gauge: float = 0.0
    percentile_rank: float = 0.0
    strong_skills: List[str] = []
    weak_skills: List[str] = []
    skill_accuracy_breakdown: Dict[str, float] = {}
    questions_attempted: int = 0
    questions_correct: int = 0
    questions_wrong: int = 0
    question_details: List[UserQuestionDetail] = []
    assessments_attempted: List[int] = []
    recommendations: List[str] = []
    activity_level: str = "Low"
    chart_data: Dict[str, Any] = Field(default_factory=dict, description="Advanced visualizations data for user dashboard.")
    class Config:
        extra = "allow"


class MasterReport(BaseModel):
    admin_report: Dict[str, Any] = Field(default_factory=dict)
    manager_reports: List[Dict[str, Any]] = Field(default_factory=list)
    user_reports: List[Dict[str, Any]] = Field(default_factory=list)
    class Config:
        extra = "allow"


class TrainResponse(BaseModel):
    status:    bool           = Field(..., description="True if training was queued.")
    message:   str            = Field(..., description="Status message.")
    client_id: int            = Field(..., description="The client ID training was started for.")
    training:  Dict[str, Any] = Field(..., description="Training metadata.")


class ReportResponse(BaseModel):
    status:    bool           = Field(..., description="True if report is available.")
    message:   str            = Field(..., description="Status message.")
    client_id: int            = Field(..., description="The client ID.")
    result:    MasterReport   = Field(..., description="Full analysis JSON from the LLM.")


class StatusResponse(BaseModel):
    status:    bool           = Field(..., description="True if client exists.")
    message:   str            = Field(..., description="Status message.")
    client_id: int            = Field(..., description="The client ID.")
    training:  Dict[str, Any] = Field(..., description="Training metadata.")


# ===========================================================================
# SECTION 3 — FastAPI Application
# ===========================================================================

app = FastAPI(
    title="Assessment Report Analysis API",
    description=(
        "## LMS Assessment Skill Intelligence API\n\n"
        "Enterprise-grade, client-isolated assessment analytics powered by "
        f"**{OLLAMA_MODEL}** (Ollama).\n\n"
        "### Two-Phase Workflow\n\n"
        "| Phase | Endpoint | Method | Purpose |\n"
        "|-------|----------|--------|---------|\n"
        "| **Train** | `/train/{client_id}` | POST | Accepts data, queues background analysis, stores results |\n"
        "| **Report** | `/report/{client_id}` | GET | Retrieves stored analysis instantly |\n\n"
        "### Client Isolation\n"
        "Every organisation (client) has its own isolated data store. "
        "Data from client `12345` is **never** mixed with client `67890`. "
        "Client IDs are integers (any digit length).\n\n"
        "### Additional Endpoints\n"
        "- `GET /report/status/{client_id}` — Check training progress\n"
        "- `GET /clients` — List all trained clients\n"
        "- `DELETE /client/{client_id}` — Remove a client's data\n"
        "- `GET /health` — System health check"
    ),
    version="2.0.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


# ===========================================================================
# SECTION 4 — API ENDPOINTS
# ===========================================================================

# ---------------------------------------------------------------------------
# POST /train/{client_id} — Phase 1: Train & Store
# ---------------------------------------------------------------------------

@app.post(
    "/train/{client_id}",
    response_model=TrainResponse,
    summary="Phase 1 — Train: Analyse & store client data",
    description=(
        "Accepts the organisation's complete question bank, assessment summary, "
        "completed user records, and individual attempt details.\n\n"
        "**The analysis runs in the background** — this endpoint returns immediately "
        "with a `queued` status. Poll `/report/status/{client_id}` to check progress.\n\n"
        "Once training completes, results are stored persistently and can be "
        "retrieved via `GET /report/{client_id}` at any time.\n\n"
        "**Client isolation**: Each `client_id` has its own dedicated storage. "
        "Re-training a client overwrites its previous results.\n\n"
        "**Concurrent safety**: Only one training job per client can run at a time."
    ),
    responses={
        200: {"description": "Training queued successfully."},
        409: {"description": "Training already in progress for this client."},
        422: {"description": "Validation error in request payload."},
        500: {"description": "Internal server error."},
    },
    tags=["Training"],
)
async def train_client(
    payload: TrainRequest,
    background_tasks: BackgroundTasks,
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """
    Accepts all four data inputs, validates them, and starts background training.
    Returns immediately with queued status.
    """
    logger.info(
        f"POST /train/{client_id} | "
        f"questions={len(payload.question_bank)} | "
        f"assessments={len(payload.assessment_summary)} | "
        f"completed_attempts={len(payload.completed_users_data)} | "
        f"attempt_details={len(payload.user_attempt_details)}"
    )

    try:
        q_bank  = [q.model_dump(by_alias=False) for q in payload.question_bank]
        a_sum   = [a.model_dump(by_alias=False) for a in payload.assessment_summary]
        c_users = [c.model_dump(by_alias=False) for c in payload.completed_users_data]
        u_att   = [d.model_dump(by_alias=False) for d in payload.user_attempt_details]

        training_meta = start_training_meta(
            client_id=client_id,
            question_bank=q_bank,
            assessment_summary=a_sum,
            completed_users_data=c_users,
            user_attempt_details=u_att,
            manager_mapping=payload.manager_mapping,
        )

        if training_meta.get("status") == TrainingStatus.QUEUED:
            background_tasks.add_task(
                run_training_pipeline,
                client_id, q_bank, a_sum, c_users, u_att, payload.manager_mapping
            )

    except RuntimeError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(exc),
        )
    except Exception as exc:
        logger.exception(f"Failed to start training for client {client_id}: {exc}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to start training: {exc}",
        )

    return TrainResponse(
        status=True,
        message=(
            f"Training queued for client {client_id}. "
            f"Poll GET /report/status/{client_id} to check progress."
        ),
        client_id=client_id,
        training=training_meta,
    )


# ---------------------------------------------------------------------------
# POST /train/{client_id}/start — Batched Phase 1: Initialize Session
# ---------------------------------------------------------------------------

@app.post(
    "/train/{client_id}/start",
    summary="Batched Training — Phase 1: Initialize session",
    description=(
        "Initializes a new training session for the client.\n\n"
        "Creates a fresh accumulator on disk and clears any previous partial data.\n"
        "Must be called before sending batches via `/train/{client_id}/batch`."
    ),
    responses={
        200: {"description": "Session initialized."},
        500: {"description": "Internal server error."},
    },
    tags=["Batched Training"],
)
async def train_start(
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """Initialize a batched training session for the client."""
    logger.info(f"POST /train/{client_id}/start")

    try:
        meta = start_client_session(client_id)
    except Exception as exc:
        logger.exception(f"Failed to start session for client {client_id}: {exc}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to start session: {exc}",
        )

    return {
        "status": True,
        "message": f"Training session initialized for client {client_id}.",
        "client_id": client_id,
        "training": meta,
    }


# ---------------------------------------------------------------------------
# POST /train/{client_id}/batch — Batched Phase 2: Append Batch
# ---------------------------------------------------------------------------

@app.post(
    "/train/{client_id}/batch",
    summary="Batched Training — Phase 2: Send a data batch",
    description=(
        "Appends a batch of questions and their related user attempt details to the\n"
        "client's accumulator on disk.\n\n"
        "Call this endpoint repeatedly with successive batches of 500 questions.\n"
        "Each batch is written to disk immediately so memory stays low.\n\n"
        "**Prerequisites**: Call `POST /train/{client_id}/start` first."
    ),
    responses={
        200: {"description": "Batch appended successfully."},
        409: {"description": "No active session — call /start first."},
        500: {"description": "Internal server error."},
    },
    tags=["Batched Training"],
)
async def train_batch(
    payload: BatchTrainRequest,
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """Append a batch of question bank + user attempt data to the client's accumulator."""
    logger.info(
        f"POST /train/{client_id}/batch | "
        f"questions={len(payload.question_bank)} | "
        f"attempts={len(payload.user_attempt_details)}"
    )

    try:
        q_bank = [q.model_dump(by_alias=False) for q in payload.question_bank]
        u_att  = [d.model_dump(by_alias=False) for d in payload.user_attempt_details]

        batch_info = append_batch_data(client_id, q_bank, u_att)

    except RuntimeError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(exc),
        )
    except Exception as exc:
        logger.exception(f"Failed to append batch for client {client_id}: {exc}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to append batch: {exc}",
        )

    return {
        "status": True,
        "message": f"Batch #{batch_info['batch_number']} appended for client {client_id}.",
        "client_id": client_id,
        "batch_info": batch_info,
    }


# ---------------------------------------------------------------------------
# POST /train/{client_id}/finalize — Batched Phase 3: Compute Results
# ---------------------------------------------------------------------------

@app.post(
    "/train/{client_id}/finalize",
    summary="Batched Training — Phase 3: Finalize & compute reports",
    description=(
        "Runs the full training pipeline on all accumulated batch data.\n\n"
        "Reads the accumulator from disk, processes all questions and attempts,\n"
        "computes charts and reports, saves `result.json`, then cleans up.\n\n"
        "After this completes, results are available via `GET /report/{client_id}`.\n\n"
        "**Prerequisites**: At least one batch must have been sent via\n"
        "`POST /train/{client_id}/batch`."
    ),
    responses={
        200: {"description": "Training finalized successfully."},
        409: {"description": "No active session or no data to finalize."},
        500: {"description": "Internal server error."},
    },
    tags=["Batched Training"],
)
async def train_finalize(
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """Finalize training: run pipeline on all accumulated data."""
    logger.info(f"POST /train/{client_id}/finalize")

    try:
        summary = finalize_training(client_id)
    except RuntimeError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(exc),
        )
    except Exception as exc:
        logger.exception(f"Failed to finalize training for client {client_id}: {exc}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to finalize training: {exc}",
        )

    return {
        "status": True,
        "message": f"Training finalized for client {client_id}.",
        "client_id": client_id,
        "summary": summary,
    }


# ---------------------------------------------------------------------------
# GET /report/{client_id} — Phase 2: Retrieve Analysis
# ---------------------------------------------------------------------------

async def _fetch_raw_result(client_id: int) -> dict:
    """Helper to fetch and validate training status, returning raw dict."""
    meta = get_training_status(client_id)

    if meta is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=(
                f"No data found for client {client_id}. "
                f"Please train first via POST /train/{client_id}."
            ),
        )

    current_status = meta.get("status")

    if current_status == TrainingStatus.QUEUED:
        raise HTTPException(
            status_code=status.HTTP_202_ACCEPTED,
            detail=f"Training is queued for client {client_id}. Please wait.",
        )

    if current_status == TrainingStatus.PROCESSING:
        raise HTTPException(
            status_code=status.HTTP_202_ACCEPTED,
            detail=f"Training is still in progress for client {client_id}. Please wait.",
        )

    if current_status == TrainingStatus.FAILED:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=(
                f"Training failed for client {client_id}. "
                f"Error: {meta.get('error', 'Unknown')}. "
                f"Please re-train via POST /train/{client_id}."
            ),
        )

    # Status is COMPLETED — fetch the result
    result = get_analysis_result(client_id)

    if result is None:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Analysis result file is missing for client {client_id}. Please re-train.",
        )
        
    return result

@app.get(
    "/report/{client_id}",
    summary="Phase 2 — Report: Retrieve stored analysis",
    description=(
        "Returns the pre-computed analysis for a specific client.\n\n"
        "**Prerequisites**: The client must have completed training via "
        "`POST /train/{client_id}` first.\n\n"
        "The response contains the full structured JSON analysis including:\n"
        "- Question → Skill mapping with Bloom's Taxonomy\n"
        "- Organisational skill intelligence\n"
        "- Assessment health reports\n"
        "- Integrity flags\n"
        "- Executive summary"
    ),
    responses={
        200: {"description": "Analysis retrieved successfully."},
        404: {"description": "Client not found or training not completed."},
        202: {"description": "Training still in progress."},
    },
    tags=["Report"],
)
async def get_report(
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """Returns the saved analysis result for a specific client, optimized for Admin view."""
    logger.info(f"GET /report/{client_id}")
    
    result = await _fetch_raw_result(client_id)

    # We just return the precomputed charts for admin view
    admin_data = result.get("admin_report", {})
    
    return {
        "status": True,
        "message": f"Analysis report for client {client_id} retrieved successfully.",
        "client_id": client_id,
        "result": admin_data,
    }

# ---------------------------------------------------------------------------
# ROLE-BASED ACCESS (Subsets of the master report)
# ---------------------------------------------------------------------------

@app.get(
    "/report/admin/{client_id}",
    summary="Admin Report: System-level insights",
    tags=["Report"],
)
async def get_admin_report(client_id: int = Path(..., ge=1)):
    """Convenience alias for the master report."""
    return await get_report(client_id)


@app.get(
    "/report/manager/{client_id}/{manager_id}",
    summary="Manager Report: Team-level insights",
    tags=["Report"],
)
async def get_manager_report(
    client_id: int = Path(..., ge=1),
    manager_id: str = Path(..., description="ID of the manager."),
):
    """Returns the manager's team report from the master analysis."""
    result = await _fetch_raw_result(client_id)
    manager_reports = result.get("manager_reports", [])
    
    manager_data = next((m for m in manager_reports if str(m.get("manager_id")) == str(manager_id)), None)
    
    if not manager_data:
        raise HTTPException(
            status_code=404,
            detail=f"Manager {manager_id} not found in analysis for client {client_id}."
        )
        
    return {
        "status": True,
        "message": f"Manager report for {manager_id} retrieved.",
        "client_id": client_id,
        "result": manager_data
    }


@app.get(
    "/report/user/{client_id}/{user_id}",
    summary="User Report: Individual insights",
    tags=["Report"],
)
async def get_user_report(
    client_id: int = Path(..., ge=1),
    user_id: str = Path(..., description="ID of the user."),
):
    """Returns the individual user report from the master analysis."""
    result = await _fetch_raw_result(client_id)
    user_reports = result.get("user_reports", [])
    
    user_data = next((u for u in user_reports if str(u.get("user_id")) == str(user_id)), None)
    
    if not user_data:
        raise HTTPException(
            status_code=404,
            detail=f"User {user_id} not found in analysis for client {client_id}."
        )
        
    return {
        "status": True,
        "message": f"User report for {user_id} retrieved.",
        "client_id": client_id,
        "result": user_data
    }


# ---------------------------------------------------------------------------
# GET /report/status/{client_id} — Check Training Status
# ---------------------------------------------------------------------------

@app.get(
    "/report/status/{client_id}",
    response_model=StatusResponse,
    summary="Check training status",
    description=(
        "Returns the current training status for a client.\n\n"
        "**Possible statuses:**\n"
        "- `queued` — Training is scheduled\n"
        "- `processing` — LLM analysis is running\n"
        "- `completed` — Analysis is done and available\n"
        "- `failed` — Training encountered an error"
    ),
    responses={
        200: {"description": "Status retrieved."},
        404: {"description": "Client not found."},
    },
    tags=["Report"],
)
async def check_status(
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """Returns the training status metadata for a specific client."""

    meta = get_training_status(client_id)

    if meta is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No data found for client {client_id}.",
        )

    return StatusResponse(
        status=True,
        message=f"Status for client {client_id}: {meta.get('status')}.",
        client_id=client_id,
        training=meta,
    )


# ---------------------------------------------------------------------------
# GET /clients — List All Trained Clients
# ---------------------------------------------------------------------------

@app.get(
    "/clients",
    summary="List all trained clients",
    description=(
        "Returns a list of all clients that have training data stored, "
        "with their current status and metadata. "
        "Useful for admin dashboards and bulk status checks."
    ),
    tags=["Management"],
)
async def list_clients():
    """Lists all clients with training data."""
    clients = list_all_clients()
    return {
        "status":       True,
        "message":      f"{len(clients)} client(s) found.",
        "total_clients": len(clients),
        "clients":      clients,
    }


# ---------------------------------------------------------------------------
# DELETE /client/{client_id} — Remove Client Data
# ---------------------------------------------------------------------------

@app.delete(
    "/client/{client_id}",
    summary="Delete client data",
    description=(
        "Permanently removes all stored data for a specific client, "
        "including analysis results, input snapshots, and metadata.\n\n"
        "**Cannot delete while training is in progress.**"
    ),
    responses={
        200: {"description": "Client data deleted."},
        404: {"description": "Client not found."},
        409: {"description": "Training in progress — cannot delete."},
    },
    tags=["Management"],
)
async def delete_client(
    client_id: int = Path(..., ge=1, description="Unique integer client/organisation ID."),
):
    """Deletes all stored data for a client."""

    logger.info(f"DELETE /client/{client_id}")

    try:
        deleted = delete_client_data(client_id)
    except RuntimeError as exc:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(exc),
        )

    if not deleted:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"No data found for client {client_id}.",
        )

    return {
        "status":    True,
        "message":   f"All data for client {client_id} has been permanently deleted.",
        "client_id": client_id,
    }


# ---------------------------------------------------------------------------
# GET /health — System Health Check
# ---------------------------------------------------------------------------

@app.get(
    "/health",
    summary="System health check",
    description="Returns API status, Ollama connectivity, and model availability.",
    tags=["System"],
)
async def health_check():
    """Liveness probe — verifies API and Ollama are operational."""

    ollama_health = check_ollama_health()
    clients = list_all_clients()

    completed   = sum(1 for c in clients if c.get("status") == TrainingStatus.COMPLETED)
    processing  = sum(1 for c in clients if c.get("status") == TrainingStatus.PROCESSING)
    failed      = sum(1 for c in clients if c.get("status") == TrainingStatus.FAILED)

    return {
        "status":       "ok",
        "api":          "Assessment Report Analysis API",
        "version":      "2.0.0",
        "client_stats": {
            "total":       len(clients),
            "completed":   completed,
            "processing":  processing,
            "failed":      failed,
        },
        **ollama_health,
    }


# ===========================================================================
# SECTION 5 — TEST ENDPOINT (HARDCODED DATA — FOR DEVELOPMENT ONLY)
# ===========================================================================

TEST_QUESTION_BANK = {
    "status": True, "message": "Records available",
    "data": [
        {"id": "ey_q1", "questionText": "Write a review for assign Storigo in your words!", "questionType": 1, "explanation": "", "complexity": 8, "marks": 3, "correctAnswer": "", "questionOption": ""},
        {"id": "ey_q2", "questionText": "Under education loan scheme as per RBI norms for secured loan from Nationalised Banks, what type of mortgage is done by bank for overseas loan ?", "questionType": 2, "explanation": "", "complexity": 8, "marks": 3, "correctAnswer": "", "questionOption": "Conventional mortgages,Equity Mortgage,Jumbo mortgages,Registered mortgage,Simple Mortgage"},
        {"id": "ey_q3", "questionText": "Make addition Checkbox;(Make addition : 2 + 2 = ?)", "questionType": 3, "explanation": "", "complexity": 6, "marks": 1, "correctAnswer": "FOUR", "questionOption": "FOUR,ONE,THREE,TWO"},
        {"id": "ey_q4", "questionText": "Make addition Drop down text (Make addition : 2 + 2 = ?)", "questionType": 4, "explanation": "", "complexity": 6, "marks": 1, "correctAnswer": "FOUR", "questionOption": "FOUR,ONE,THREE,TWO"},
        {"id": "ey_q5", "questionText": "is this correct = 1+1=2", "questionType": 5, "explanation": "", "complexity": 6, "marks": 1, "correctAnswer": "True", "questionOption": "True, False"},
        {"id": "ey_q6", "questionText": "Below some pronouns are given that can be used with Have or Has. Move the pronouns that are used with have to the left and pronouns used with has to the right.", "questionType": 7, "explanation": "I have only 100 rupees today.", "complexity": 48, "marks": 3, "correctAnswer": "", "questionOption": "He,I,It,She,They,We,You"},
        {"id": "ey_q7", "questionText": "Match the sentence on the left to its correct type on the right.", "questionType": 8, "explanation": "", "complexity": 32, "marks": 3, "correctAnswer": "land,air,water,space", "questionOption": "car,plane,ship,rocket"},
        {"id": "ey_q8", "questionText": "Sort Alphabet", "questionType": 10, "explanation": "", "complexity": 6, "marks": 1, "correctAnswer": "A,B,C,D,E", "questionOption": "A,E,C,D,B"},
        {"id": "ey_q9", "questionText": "Professionals _______ teachers and lawyers should have ______ communication skills.", "questionType": 11, "explanation": "", "complexity": 7, "marks": 2, "correctAnswer": "such as, good", "questionOption": "as,bad,good,such as,such like"},
    ],
    "TotalQuestion": 9,
}

TEST_ASSESSMENT_SUMMARY = {
    "status": True, "message": "Records available",
    "data": [
        {"id": 970, "name": "Assesment on quiz attempt", "enrolled": 2, "completed": 2, "notStarted": 0,
         "userAssignData": [{"name": "Enrolled", "value": 2}, {"name": "Completed", "value": 2}, {"name": "Not Started", "value": 0}],
         "learningData": {"learningAccuracyTotalRight": 15, "learningAccuracyTotalWrong": 8, "learningAccuracyRightPer": 65.22, "learningAccuracyWrongPer": 34.78}},
        {"id": 948, "name": "Assesment Classic", "enrolled": 2, "completed": 1, "notStarted": 1,
         "userAssignData": [{"name": "Enrolled", "value": 2}, {"name": "Completed", "value": 1}, {"name": "Not Started", "value": 1}],
         "learningData": {"learningAccuracyTotalRight": 5, "learningAccuracyTotalWrong": 19, "learningAccuracyRightPer": 20.83, "learningAccuracyWrongPer": 79.17}},
    ],
    "assessmentCount": 2,
}

TEST_COMPLETED_USERS = {
    "status": True, "message": "Records available",
    "data": [
        {"id": 90, "assessmentId": 970, "userId": 8028, "userName": "Hari Singh", "quizName": "Assesment on quiz attempt", "totalMarks": 3, "obtainedMarks": 3, "totalObtPercentage": 100, "pass": True, "attemptNumber": "", "isSection": 0, "isSubmitType": 1, "grade": ""},
        {"id": 89, "assessmentId": 970, "userId": 8028, "userName": "Hari Singh", "quizName": "Assesment on quiz attempt", "totalMarks": 3, "obtainedMarks": 0, "totalObtPercentage": 0, "pass": False, "attemptNumber": "", "isSection": 0, "isSubmitType": 1, "grade": ""},
    ],
    "assessmentName": "Assesment on quiz attempt", "assessmentCount": 2,
}

TEST_USER_ATTEMPT_DETAILS = [
    {
        "status": True, "message": "Records available", "userName": "Hari Singh",
        "totalScore": 12, "obtainScore": 9, "userPercentage": 75, "passingMarks": 50,
        "assessmentId": 970, "assessmentName": "Assesment on quiz attempt", "contentId": 970,
        "quizName": "Assesment on quiz attempt", "isProctoring": 2, "isGamification": 2,
        "totalTabChanged": 0, "totalFaceRemoved": 0,
        "quizStartTime": "30-05-2023 10:13:10", "quizEndTime": "30-05-2023 10:13:32",
        "quizDuration": 22, "galleryDetailId": 0, "proctoringConfidence": 100,
        "data": [
            {"question_id": 5677, "question_name": "Make subtraction of Two Number : 20 - 35 = ?", "question_type": 2, "question_explanation": "Make subtraction of Two Number : 20 - 35 = -15", "is_attempted": True, "correct_answer": "20948", "user_answer": "20948", "quesType": "2", "isGrade": 0, "gradeMark": 0, "total_marks": 3, "obtained_marks": 3,
             "question_option": [
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20946, "question_option": "15", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20947, "question_option": "-20", "question_option_image": ""},
                 {"correct_answer_option": True, "user_answer": True, "option_id": 20948, "question_option": "-15", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20949, "question_option": "35", "question_option_image": ""},
             ], "user_question_option": "-15", "marks_range": []},
            {"question_id": 5676, "question_name": "Make Addition Of two Number : 10 + 21 = ?", "question_type": 2, "question_explanation": "Make Addition Of two Number : 10 + 21 = 31", "is_attempted": True, "correct_answer": "20943", "user_answer": "20943", "quesType": "2", "isGrade": 0, "gradeMark": 0, "total_marks": 3, "obtained_marks": 3,
             "question_option": [
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20942, "question_option": "20", "question_option_image": ""},
                 {"correct_answer_option": True, "user_answer": True, "option_id": 20943, "question_option": "31", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20944, "question_option": "25", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20945, "question_option": "33", "question_option_image": ""},
             ], "user_question_option": "31", "marks_range": []},
            {"question_id": 5664, "question_name": "<p>Number Addition &nbsp;:- 10 +5</p>", "question_type": 2, "question_explanation": "", "is_attempted": True, "correct_answer": "20922", "user_answer": "20922", "quesType": "2", "isGrade": 0, "gradeMark": 0, "total_marks": 3, "obtained_marks": 3,
             "question_option": [
                 {"correct_answer_option": True, "user_answer": True, "option_id": 20922, "question_option": "15", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20923, "question_option": "10", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20924, "question_option": "5", "question_option_image": ""},
                 {"correct_answer_option": False, "user_answer": False, "option_id": 20925, "question_option": "20", "question_option_image": ""},
             ], "user_question_option": "15", "user_question_option_image": "", "marks_range": []},
        ],
    }
]