"""Pydantic request/response models for the /api/exam/ router (S4-5).

Templates are saved from the canvas with a full-replace PUT (R5.2): the client owns
stable UUIDs for questions / response areas / boundaries so the Supabase ids line up
with the Neo4j join keys (exam_questions.id ↔ Question|Part.uuid_string,
exam_response_areas.id ↔ Region.uuid_string — see spec §2). Granular mark-scheme edits
go through PATCH /api/exam/questions/{qid}.

Models mirror the columns in volumes/db/cc/72-exam-marker.sql. They are intentionally
permissive (most fields optional) so the canvas can round-trip partial state during
authoring without the API rejecting work-in-progress.
"""
from __future__ import annotations

from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, Field


# ─── Templates ─────────────────────────────────────────────────────────────────

class CreateTemplateRequest(BaseModel):
    title: str
    subject: Optional[str] = None
    # Catalogue paper (eb_exams) the template maps, when chosen from the catalogue (R2.2).
    exam_id: Optional[str] = None
    # Denormalised onto the template for the Neo4j join (eb_exams.exam_code ↔ ExamPaper.exam_code).
    # If exam_id is given but exam_code is omitted, the API resolves it from the catalogue.
    exam_code: Optional[str] = None
    # Uploaded PDF (files.id) for an ad-hoc paper (R2.2).
    source_file_id: Optional[str] = None
    page_count: Optional[int] = None
    # Active institute (R1.4/R5.5). Validated against the caller's memberships; never trusted
    # as the authorization signal. Optional when the caller belongs to exactly one institute.
    institute_id: Optional[str] = None


class UpdateTemplateMetaRequest(BaseModel):
    """Template-level fields that a full-replace PUT may also update alongside the canvas."""
    title: Optional[str] = None
    subject: Optional[str] = None
    page_count: Optional[int] = None
    status: Optional[Literal["draft", "ready", "archived"]] = None


# ─── Canvas entities (children of a template) ────────────────────────────────────

class QuestionPayload(BaseModel):
    # Client-supplied stable UUID (== Neo4j Question|Part.uuid_string). Optional on first save.
    id: Optional[str] = None
    parent_id: Optional[str] = None
    label: str
    order: int = 0
    max_marks: float = 0
    answer_type: Optional[Literal["written", "mcq", "short", "diagram"]] = None
    mcq_options: Optional[Any] = None
    mark_scheme: Dict[str, Any] = Field(default_factory=dict)
    is_container: bool = False
    spec_ref: Optional[str] = None
    # Drawn Part box geometry (73-exam-marker-regions.sql). Null for derived main questions.
    bounds: Optional[Dict[str, Any]] = None  # {x,y,w,h}
    page: Optional[int] = None
    # S5 AI/manual seam + provenance. Existing manual rows default to authoritative.
    source: Literal["manual", "ai"] = "manual"
    confirmed: bool = True
    confidence: Optional[float] = Field(default=None, ge=0, le=1)
    derivation: Optional[str] = None


class ResponseAreaPayload(BaseModel):
    id: Optional[str] = None  # == Neo4j Region.uuid_string (only response/context project)
    question_id: str
    page: int
    bounds: Dict[str, Any]  # {x,y,w,h}
    # S4-9 taxonomy (73-exam-marker-regions.sql): response/context graded-or-stimulus;
    # question_number/mark_area = physical metadata; reference = student resource; furniture = ignore.
    kind: Literal["response", "context", "question_number", "mark_area", "reference", "furniture"]
    response_form: Optional[
        Literal["lines", "answer-box", "working", "diagram", "tick-boxes", "table", "blanks"]
    ] = None
    # Optional Context differentiation (v1 generic; future graph/chart/data_table/diagram/code_block/passage).
    context_type: Optional[str] = None
    source: Literal["manual", "ai"] = "manual"
    confirmed: bool = True
    confidence: Optional[float] = Field(default=None, ge=0, le=1)
    # Only meaningful for kind='mark_area': part_marks|question_total|grader_box.
    mark_subtype: Optional[Literal["part_marks", "question_total", "grader_box"]] = None
    derivation: Optional[str] = None


class BoundaryPayload(BaseModel):
    id: Optional[str] = None
    question_id: Optional[str] = None
    label: Optional[str] = None
    page_index: int
    y: float
    bounds: Optional[Dict[str, Any]] = None
    source: Literal["manual", "ai"] = "manual"
    confirmed: bool = True
    confidence: Optional[float] = Field(default=None, ge=0, le=1)
    derivation: Optional[str] = None


class TemplateLayoutPayload(BaseModel):
    id: Optional[str] = None
    page_index: int
    role: Optional[str] = None
    margin_left: Optional[float] = None
    margin_right: Optional[float] = None
    margin_top: Optional[float] = None
    margin_bottom: Optional[float] = None
    margins_enabled: bool = True
    source: Literal["manual", "ai"] = "manual"
    confirmed: bool = True
    confidence: Optional[float] = Field(default=None, ge=0, le=1)
    derivation: Optional[str] = None
    meta: Dict[str, Any] = Field(default_factory=dict)


class TemplateReplaceRequest(BaseModel):
    """Full-replace canvas save (R5.2 primary path). All children are replaced wholesale."""
    meta: Optional[UpdateTemplateMetaRequest] = None
    questions: List[QuestionPayload] = Field(default_factory=list)
    response_areas: List[ResponseAreaPayload] = Field(default_factory=list)
    boundaries: List[BoundaryPayload] = Field(default_factory=list)
    layout: List[TemplateLayoutPayload] = Field(default_factory=list)


class PatchQuestionRequest(BaseModel):
    """Incremental mark-scheme / spec-ref edit (R5.2 granular path)."""
    label: Optional[str] = None
    order: Optional[int] = None
    max_marks: Optional[float] = None
    answer_type: Optional[Literal["written", "mcq", "short", "diagram"]] = None
    mcq_options: Optional[Any] = None
    mark_scheme: Optional[Dict[str, Any]] = None
    is_container: Optional[bool] = None
    spec_ref: Optional[str] = None


# ─── Marking batches & marks ─────────────────────────────────────────────────

class CreateBatchRequest(BaseModel):
    template_id: str
    # When a class is given, the roster (class_students, status='active') is materialised as
    # student_submissions(status='absent') so every enrolled student appears in results (A7).
    class_id: Optional[str] = None
    title: Optional[str] = None


class MarkUpsertRequest(BaseModel):
    """Upsert one mark entry (PUT /marks/{id}; id is the mark_entry uuid).

    batch_id is derived server-side from the submission, so the client never sets the RLS
    scoping key. submission_id + question_id identify what is being marked.
    """
    submission_id: str
    question_id: str
    awarded_marks: float = 0
    mark_scheme_detail: Optional[Dict[str, Any]] = None
    annotation_shape_ids: Optional[Any] = None
    comment: Optional[str] = None
    confirmed: Optional[bool] = None
