"""
ABOS AI Worker – Pydantic Schemas  (Unified Finance Schema v2)
==============================================================
All request / response models live here so they can be imported
from a single place without introducing circular dependencies.

Schema alignment
----------------
These models align 1-to-1 with the Spring Boot "Unified Finance" star schema:

  ┌─────────────────────────────────────────────────────────────┐
  │  FastAPI model          →  Spring Boot entity / column      │
  ├─────────────────────────────────────────────────────────────┤
  │  StatementResult        →  FinanceStatement (fact table)    │
  │  TransactionItem        →  FinanceTransaction (fact table)  │
  │  entityType             →  FinanceStatement.entityType      │
  │  entityId               →  FinanceStatement.entityId        │
  │  associatedCardNumber   →  FinanceTransaction.cardNumber    │
  │  homeTaxPercent         →  FinanceTransaction.taxPercent    │
  └─────────────────────────────────────────────────────────────┘

Decimal / JSON serialisation contract
--------------------------------------
All monetary and percentage fields use Python ``Decimal`` to avoid
IEEE-754 float drift.  The ``model_config`` instructs Pydantic to
serialise Decimal values as JSON numbers (not strings), which is what
the Spring Boot ``@JsonDeserialize`` mapper expects.
"""

from __future__ import annotations

import enum
from decimal import Decimal
from typing import Any, Literal, Optional

from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import ConfigDict


# ===========================================================================
# Enumerations
# ===========================================================================

class JobStatus(str, enum.Enum):
    """
    Lifecycle states of an AI extraction job.

    State-machine transitions (happy path):
        PENDING → EXTRACTING → VERIFYING → POSTING → COMPLETED

    Diverging paths:
        EXTRACTING  → FAILED       (LLM error / unreadable PDF)
        VERIFYING   → NEEDS_REVIEW (empty transaction list after extraction)
        POSTING     → FAILED       (all webhook retries exhausted)

    NEEDS_REVIEW requires manual human intervention via the HITL endpoint
    before the job can transition to POSTING → COMPLETED.
    """
    PENDING      = "PENDING"       # Job created; PDF upload complete
    EXTRACTING   = "EXTRACTING"    # LLM call in progress
    VERIFYING    = "VERIFYING"     # Pre-webhook validation running
    POSTING      = "POSTING"       # Webhook delivery in progress
    COMPLETED    = "COMPLETED"     # Spring Boot acknowledged receipt
    NEEDS_REVIEW = "NEEDS_REVIEW"  # Requires HITL: e.g. empty transactions
    FAILED       = "FAILED"        # Unrecoverable error; see error_detail


class EntityType(str, enum.Enum):
    """
    Discriminator that drives branching extraction logic.
    Passed from Spring Boot in the ExtractRequest payload.
    """
    BANK        = "Bank"
    CREDIT_CARD = "CreditCard"


# ===========================================================================
# Domain Models  (the "Unified Finance" schema objects)
# ===========================================================================

class TransactionItem(BaseModel):
    """
    Represents a single debit / credit line on a financial statement.

    Branching rules (enforced in extraction logic, not here):
      - Bank statements       → associatedCardNumber = null, homeTaxPercent = null
      - Credit card statements → both fields populated where determinable
    """
    model_config = ConfigDict(
        populate_by_name=True,
        # Serialise Decimal → JSON number (not string) so Spring Boot
        # @JsonDeserialize receives a proper numeric type.
        json_encoders={Decimal: float},
    )

    date: str = Field(
        ...,
        description="Transaction date in strict ISO-8601 format: YYYY-MM-DD.",
        examples=["2024-03-15"],
        pattern=r"^\d{4}-\d{2}-\d{2}$",
    )
    description: str = Field(
        ...,
        description="Merchant name or narrative as printed on the statement.",
    )
    debit: Optional[Decimal] = Field(
        default=None,
        description="Amount debited (money leaving the account). "
                    "Null if this row is a credit. Always a positive number.",
        ge=Decimal("0"),
    )
    credit: Optional[Decimal] = Field(
        default=None,
        description="Amount credited (money entering the account). "
                    "Null if this row is a debit. Always a positive number.",
        ge=Decimal("0"),
    )
    balance: Optional[Decimal] = Field(
        default=None,
        description="Running balance after this transaction.",
    )
    # ------------------------------------------------------------------
    # Credit-Card-only fields
    # Bank statements must set these to null (enforced by extraction logic).
    # ------------------------------------------------------------------
    associated_card_number: Optional[str] = Field(
        default=None,
        alias="associatedCardNumber",
        description=(
            "Last 4 digits (or masked number) of the card that generated "
            "this transaction. Applicable to CreditCard statements only; "
            "must be null for Bank statements."
        ),
        examples=["**** **** **** 4321", None],
    )
    home_tax_percent: Optional[Decimal] = Field(
        default=None,
        alias="homeTaxPercent",
        description=(
            "Tax percentage applied to this line item (e.g. 15.00 for 15%). "
            "Applicable to CreditCard statements only; must be null for Bank "
            "statements. Valid range: 0 – 100 inclusive."
        ),
        ge=Decimal("0"),
        le=Decimal("100"),
    )

    @model_validator(mode="after")
    def validate_debit_or_credit(self) -> TransactionItem:
        """At least one of debit or credit must be non-null."""
        if self.debit is None and self.credit is None:
            raise ValueError(
                "A TransactionItem must have at least one of 'debit' or 'credit' set."
            )
        return self


class StatementResult(BaseModel):
    """
    Top-level extracted result for a single financial statement.

    Aligns with Spring Boot's FinanceStatement fact table.
    """
    model_config = ConfigDict(
        populate_by_name=True,
        json_encoders={Decimal: float},
    )

    # ------------------------------------------------------------------
    # Discriminator & FK reference (provided by Spring Boot trigger)
    # ------------------------------------------------------------------
    entity_type: EntityType = Field(
        ...,
        alias="entityType",
        description=(
            "Type of financial entity this statement belongs to. "
            "Drives branching extraction logic."
        ),
    )
    entity_id: Optional[int] = Field(
        default=None,
        alias="entityId",
        description=(
            "Primary key of the entity (BankAccount or CreditCard) "
            "in the Spring Boot database. Echoed back so Spring Boot "
            "can persist the statement against the correct entity."
        ),
        gt=0,
    )

    # ------------------------------------------------------------------
    # Statement-level fields
    # ------------------------------------------------------------------
    account_holder: Optional[str] = Field(
        default=None,
        alias="accountHolder",
        description="Full name of the account holder as printed on the statement.",
    )
    account_number: Optional[str] = Field(
        default=None,
        alias="accountNumber",
        description="Account or card number (may be masked, e.g. **** 1234).",
    )
    statement_period: str = Field(
        ...,
        alias="statementPeriod",
        description=(
            "The billing / statement period in YYYY-MM format. "
            "Validated against the pattern before webhook dispatch."
        ),
        examples=["2024-03"],
        pattern=r"^\d{4}-\d{2}$",
    )
    opening_balance: Optional[Decimal] = Field(
        default=None,
        alias="openingBalance",
        description="Balance at the start of the statement period.",
    )
    closing_balance: Optional[Decimal] = Field(
        default=None,
        alias="closingBalance",
        description="Balance at the end of the statement period.",
    )
    currency: Optional[str] = Field(
        default=None,
        description="ISO-4217 currency code (e.g. 'PKR', 'USD').",
        max_length=3,
        examples=["PKR", "USD"],
    )
    transactions: list[TransactionItem] = Field(
        default_factory=list,
        description=(
            "Ordered list of transaction line items extracted from the statement. "
            "An empty list triggers a NEEDS_REVIEW status transition."
        ),
    )

    @field_validator("statement_period", mode="before")
    @classmethod
    def normalise_statement_period(cls, v: Any) -> str:
        """
        Accept both 'YYYY-MM' and 'YYYY-MM-DD' and normalise to 'YYYY-MM'.
        Raises ValueError for anything else so the pre-webhook validation
        catches it early.
        """
        if isinstance(v, str):
            # Accept 'YYYY-MM-DD' → truncate to 'YYYY-MM'
            if len(v) == 10 and v[4] == "-" and v[7] == "-":
                return v[:7]
        return v  # pattern validator will reject malformed values downstream


# ===========================================================================
# Webhook Payload  (outbound → Spring Boot)
# ===========================================================================

class AiExtractionWebhookPayload(BaseModel):
    """
    Payload POSTed to Spring Boot:
        POST /api/finance/ai-processing/event

    Required headers (injected by post_to_webhook, NOT part of this model):
        X-Event           : PROCESS_AI_BANK_STATEMENT
        X-Webhook-Secret  : <shared secret from config>
        X-Idempotency-Key : <document_id>

    JSON body shape expected by Spring Boot:
    {
        "tenantId"        : "...",
        "userId"          : "...",
        "documentId"      : "...",
        "entityType"      : "Bank" | "CreditCard",
        "entityId"        : 42,
        "bankStatementData": { ...StatementResult... }
    }
    """
    model_config = ConfigDict(
        populate_by_name=True,
        json_encoders={Decimal: float},
    )

    tenant_id: str = Field(..., alias="tenantId")
    user_id: str = Field(..., alias="userId")
    document_id: str = Field(..., alias="documentId")
    entity_type: EntityType = Field(..., alias="entityType")
    entity_id: Optional[int] = Field(default=None, alias="entityId")
    bank_statement_data: StatementResult = Field(..., alias="bankStatementData")


# ===========================================================================
# /upload  –  request / response
# ===========================================================================

class UploadResponse(BaseModel):
    """Returned by POST /upload after a PDF has been ingested."""
    document_id: str = Field(
        ...,
        description="UUID that uniquely identifies the uploaded document in "
                    "the transient in-memory store.",
        examples=["3fa85f64-5717-4562-b3fc-2c963f66afa6"],
    )
    filename: str = Field(..., description="Original filename of the uploaded PDF.")
    page_count: int = Field(..., description="Number of pages extracted from the PDF.")
    status: JobStatus = Field(default=JobStatus.PENDING)


# ===========================================================================
# /extract  –  request / response
# ===========================================================================

class ExtractRequest(BaseModel):
    """
    Payload sent by Spring Boot to trigger LLM-based data extraction.

    Now includes entity_type (and optional entity_id) so the extraction
    logic can branch between Bank and CreditCard prompt strategies.
    """
    model_config = ConfigDict(populate_by_name=True)

    document_id: str = Field(..., alias="documentId")
    openrouter_key: str = Field(
        ...,
        alias="openrouterKey",
        description="Live OpenRouter API key supplied by Spring Boot. "
                    "Never stored persistently by this service.",
    )
    entity_type: EntityType = Field(
        ...,
        alias="entityType",
        description="Drives branching extraction logic (Bank vs CreditCard).",
    )
    entity_id: Optional[int] = Field(
        default=None,
        alias="entityId",
        description="PK of the entity in the Spring Boot DB; echoed in the webhook.",
    )
    tenant_id: str = Field(..., alias="tenantId")
    user_id: str = Field(..., alias="userId")
    model: Optional[str] = Field(
        default=None,
        description="Optional OpenRouter model override.",
    )
    # Optional: Spring Boot may supply the webhook URL per-request,
    # or the service can use the value from config.
    webhook_url: Optional[str] = Field(
        default=None,
        alias="webhookUrl",
        description="Override for the Spring Boot webhook endpoint URL.",
    )
    auto_post: bool = Field(
        default=True,
        alias="autoPost",
        description="When True (default) the extracted result is delivered to the "
                    "Spring Boot webhook immediately (auto-save). When False, the "
                    "result is returned in the response only, for human review "
                    "before saving (HITL flow).",
    )


class ExtractResponse(BaseModel):
    """Returned by POST /extract after the LLM has processed the document."""
    model_config = ConfigDict(populate_by_name=True)

    document_id: str = Field(..., alias="documentId")
    status: JobStatus
    model_used: str = Field(..., alias="modelUsed")
    extraction_layers: list[str] = Field(
        default_factory=list,
        alias="extractionLayers",
        description=(
            "Every model identifier that contributed at least one field to the final result, "
            "in the order tried. A single-element list means the primary model alone was "
            "sufficient; more than one means the multi-layer fallback strategy filled gaps "
            "the primary pass left null."
        ),
    )
    entity_type: EntityType = Field(..., alias="entityType")
    statement_result: StatementResult = Field(..., alias="statementResult")


# ===========================================================================
# /data-points/{document_id}  –  response
# ===========================================================================

class DataPointsResponse(BaseModel):
    """Returned by GET /data-points/{document_id} for HITL review on the frontend."""
    model_config = ConfigDict(populate_by_name=True)

    document_id: str = Field(..., alias="documentId")
    status: JobStatus
    entity_type: EntityType = Field(..., alias="entityType")
    statement_result: StatementResult = Field(..., alias="statementResult")
    model_used: Optional[str] = Field(default=None, alias="modelUsed")
    extraction_layers: list[str] = Field(default_factory=list, alias="extractionLayers")


# ===========================================================================
# /human-in-loop  –  request / response
# ===========================================================================

class HumanInLoopRequest(BaseModel):
    """
    Payload sent by the React frontend after a human has reviewed / edited
    the AI-extracted data points.
    """
    model_config = ConfigDict(populate_by_name=True)

    document_id: str = Field(..., alias="documentId")
    verified_data: StatementResult = Field(
        ...,
        alias="verifiedData",
        description="The human-approved StatementResult (may include edits).",
    )
    spring_boot_webhook_url: str = Field(
        ...,
        alias="springBootWebhookUrl",
        examples=["https://erp.example.com/api/finance/ai-processing/event"],
    )
    tenant_id: str = Field(..., alias="tenantId")
    user_id: str = Field(..., alias="userId")


class HumanInLoopResponse(BaseModel):
    """Immediate 202 Accepted response sent to the React frontend."""
    message: str = Field(
        default="Verification accepted. Webhook delivery is in progress.",
    )
    document_id: str = Field(..., alias="documentId")
    model_config = ConfigDict(populate_by_name=True)


# ===========================================================================
# Internal storage model  (not exposed via API)
# ===========================================================================

class DocumentRecord(BaseModel):
    """In-memory record for a single uploaded document."""
    model_config = ConfigDict(populate_by_name=True)

    document_id: str
    filename: str
    page_count: int
    raw_text: str
    status: JobStatus = JobStatus.PENDING
    entity_type: Optional[EntityType] = None
    entity_id: Optional[int] = None
    tenant_id: Optional[str] = None
    user_id: Optional[str] = None
    statement_result: Optional[StatementResult] = None
    model_used: Optional[str] = None
    extraction_layers: list[str] = Field(default_factory=list)
    error_detail: Optional[str] = None   # Populated on FAILED / NEEDS_REVIEW


# ===========================================================================
# Email Analyzer Bot Schemas
# ===========================================================================

class EmailAnalyzeRequest(BaseModel):
    """
    Payload received from the React JS frontend to trigger email analysis
    and draft generation.
    """
    model_config = ConfigDict(populate_by_name=True)

    sender: str = Field(..., description="Email address of the sender.")
    subject: str = Field(..., description="Subject line of the email.")
    body: str = Field(..., description="Full text body of the incoming email.")
    tenant_id: str = Field(..., alias="tenantId", description="Tenant ID to match configuration context.")
    user_id: str = Field(..., alias="userId", description="User ID triggering the request.")
    
    # Auth credentials to query Spring Boot for settings
    spring_boot_auth_token: str = Field(
        ..., 
        alias="springBootAuthToken", 
        description="JWT token or API key for authenticating with the Spring Boot backend to fetch AI keys and credentials."
    )
    
    # Optional parameters
    reply_tone: Optional[str] = Field(
        default="professional", 
        alias="replyTone", 
        description="The desired tone of the response draft (e.g. professional, casual, apologetic)."
    )


class EmailAnalyzeResponse(BaseModel):
    """
    Returned to the React JS frontend containing the generated response draft details.
    """
    model_config = ConfigDict(populate_by_name=True)

    status: str = Field(..., description="Status of the operation (e.g. 'DRAFT_CREATED', 'FAILED').")
    original_subject: str = Field(..., alias="originalSubject")
    generated_reply_subject: str = Field(..., alias="generatedReplySubject")
    generated_reply_body: str = Field(..., alias="generatedReplyBody")
    draft_id: Optional[str] = Field(
        default=None, 
        alias="draftId", 
        description="The identifier of the created draft in the email provider system (e.g. MS Graph draft ID or Gmail draft ID)."
    )
    error_detail: Optional[str] = Field(default=None, alias="errorDetail")


# ===========================================================================
# Meeting Analyzer Bot Schemas
# ===========================================================================

class MeetingActionItem(BaseModel):
    """
    Represents an action item derived from the meeting transcript.
    """
    model_config = ConfigDict(populate_by_name=True)

    person_speaker: str = Field(..., alias="personSpeaker", description="The person who spoke or is assigned the responsibility.")
    summary: str = Field(..., description="Summary of the action item.")
    actual_block_of_discussion: str = Field(
        ..., 
        alias="actualBlockOfDiscussion", 
        description="The exact quote or block of text from the transcript discussing this action item."
    )


class MeetingTaskItem(BaseModel):
    """
    Structured task model designed to integrate with the Project management module.
    """
    model_config = ConfigDict(populate_by_name=True)

    title: str = Field(..., description="Title of the task.")
    description: str = Field(..., description="Detailed description of what needs to be done.")
    assignee: Optional[str] = Field(default=None, description="Suggested assignee user ID or name.")
    priority: str = Field(default="MEDIUM", description="Task priority (LOW, MEDIUM, HIGH).")
    due_date_recommendation: Optional[str] = Field(
        default=None, 
        alias="dueDateRecommendation", 
        description="ISO-8601 date recommendation (YYYY-MM-DD)."
    )
    project_name_ref: Optional[str] = Field(
        default=None, 
        alias="projectNameRef", 
        description="Name or ID of the project this task aligns with."
    )


class MeetingAnalyzeResult(BaseModel):
    """
    The full analysis result of a meeting transcription.
    """
    model_config = ConfigDict(populate_by_name=True)

    meeting_id: str = Field(..., alias="meetingId")
    title: str = Field(..., description="Identified or provided meeting title.")
    summary: str = Field(..., description="High-level summary of the meeting topics and outcomes.")
    action_items: list[MeetingActionItem] = Field(default_factory=list, alias="actionItems")
    generated_tasks: list[MeetingTaskItem] = Field(default_factory=list, alias="generatedTasks")


class MeetingAnalyzeRequest(BaseModel):
    """
    Request model received from React JS frontend to analyze meeting transcription.
    """
    model_config = ConfigDict(populate_by_name=True)

    meeting_id: str = Field(..., alias="meetingId", description="Unique ID for the meeting recording/session.")
    title: Optional[str] = Field(default="Untitled Meeting", description="Optional default title.")
    transcript: str = Field(..., description="The raw transcription text of the meeting.")
    tenant_id: str = Field(..., alias="tenantId")
    user_id: str = Field(..., alias="userId")
    spring_boot_auth_token: str = Field(..., alias="springBootAuthToken")


class MeetingAskRequest(BaseModel):
    """
    Payload for asking questions directly about the transcript.
    """
    model_config = ConfigDict(populate_by_name=True)

    meeting_id: str = Field(..., alias="meetingId")
    transcript: str = Field(..., description="The raw transcript text to search.")
    question: str = Field(..., description="The question about the meeting details.")
    spring_boot_auth_token: str = Field(..., alias="springBootAuthToken")
    tenant_id: str = Field(..., alias="tenantId")


class MeetingAskResponse(BaseModel):
    """
    Answer output back to the React UI.
    """
    model_config = ConfigDict(populate_by_name=True)

    meeting_id: str = Field(..., alias="meetingId")
    question: str = Field(..., description="The original question.")
    answer: str = Field(..., description="AI generated answer referencing speaker actions.")


# ===========================================================================
# Project/Workitem Description Enhancer
# ===========================================================================

class EnhanceTextRequest(BaseModel):
    """Payload sent by Spring Boot to draft/polish a Project or work-item description."""
    model_config = ConfigDict(populate_by_name=True)

    openrouter_key: str = Field(
        ..., alias="openrouterKey",
        description="Live OpenRouter API key supplied by Spring Boot. Never stored persistently by this service.",
    )
    model: Optional[str] = Field(default=None, description="Optional model override.")
    item_type: str = Field(
        ..., alias="itemType",
        description="What kind of record this is, e.g. 'Project', 'Feature', 'PBI', 'Task', 'Bug', 'Spike', "
                    "'Milestone', 'Activity', 'Deliverable', 'Goal', 'ToDo', 'ActionItem'.",
    )
    title: str = Field(..., description="The record's title/name — used as the subject when drafting from scratch.")
    existing_description: Optional[str] = Field(
        default=None, alias="existingDescription",
        description="Current draft text, if any. When present, the AI polishes/expands it rather than "
                    "starting from nothing.",
    )
    parent_context: Optional[str] = Field(
        default=None, alias="parentContext",
        description="Short description of the parent item/project, for grounding (e.g. 'Feature: Checkout redesign').",
    )


class EnhanceTextResponse(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    description: str = Field(..., description="The drafted or polished description text.")
    model_used: str = Field(..., alias="modelUsed")


# ===========================================================================
# AI Project Plan Generator
# ===========================================================================
# Mirrors abos-web/src/features/projects/types/index.ts's WORKITEM_TYPES/HIERARCHY exactly —
# see PlanItem.type's description for the allowed values and nesting rules. This schema is also
# the literal shape of the "paste a plan" import path on the frontend, so a plan produced by an
# EXTERNAL agent (given the documented prompt) round-trips through the same validation as one
# generated by this service.

class GenerateProjectPlanRequest(BaseModel):
    """Payload sent by Spring Boot to generate a full project plan via OpenRouter."""
    model_config = ConfigDict(populate_by_name=True)

    openrouter_key: str = Field(
        ..., alias="openrouterKey",
        description="Live OpenRouter API key supplied by Spring Boot. Never stored persistently by this service.",
    )
    model: Optional[str] = Field(default=None, description="Optional model override.")
    project_type: str = Field(
        ..., alias="projectType",
        description="'Technical', 'Business', or 'Ongoing Operations' — determines the required root item type "
                    "(Feature, Milestone, or Goal respectively).",
    )
    brief: str = Field(..., description="The user's plain-language description of the project to plan out.")


class ConvertProjectPlanRequest(BaseModel):
    """Payload sent by Spring Boot to convert an already-authored, free-text plan (pasted by the
    user — notes, an outline, a plan copied from elsewhere) into the structured JSON shape below.
    Unlike GenerateProjectPlanRequest, this does not invent new plan content — it faithfully
    transcribes what the user pasted, only inferring types/nesting where the hierarchy requires it."""
    model_config = ConfigDict(populate_by_name=True)

    openrouter_key: str = Field(..., alias="openrouterKey")
    model: Optional[str] = Field(default=None, description="Optional model override.")
    project_type: str = Field(
        ..., alias="projectType",
        description="'Technical', 'Business', or 'Ongoing Operations' — determines the required root item type.",
    )
    plan_text: str = Field(
        ..., alias="planText",
        description="The user's plain-text plan — any format (outline, bullet list, prose, notes).",
    )


class PlanItem(BaseModel):
    """One node in the plan tree — a Feature/PBI/Task/Bug/Spike/Milestone/Activity/Deliverable/
    Goal/ToDo/ActionItem. Recursive via `children`; depth and allowed nesting are validated by
    the caller (Spring Boot re-validates too — see WorkitemService.validateHierarchy), not here."""
    model_config = ConfigDict(populate_by_name=True)

    title: str
    type: str = Field(
        ...,
        description="One of: Feature, PBI, Task, Bug, Spike, Milestone, Activity, Deliverable, "
                    "Goal, ToDo, ActionItem.",
    )
    description: Optional[str] = None
    priority: str = Field(default="Medium", description="Low, Medium, High, or Critical.")
    estimate_hours: Optional[float] = Field(default=None, alias="estimateHours")
    due_date: Optional[str] = Field(default=None, alias="dueDate", description="YYYY-MM-DD, or null.")
    children: list["PlanItem"] = Field(default_factory=list)


PlanItem.model_rebuild()  # resolves the recursive "PlanItem" forward reference in children


class ProjectPlanProject(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    name: str
    type: str = Field(..., description="Technical, Business, or Ongoing Operations.")
    description: Optional[str] = None
    priority: str = Field(default="Medium")
    start_date: Optional[str] = Field(default=None, alias="startDate")
    end_date: Optional[str] = Field(default=None, alias="endDate")


class ProjectPlanResult(BaseModel):
    """Returned by POST /generate-project-plan. Also the exact shape a human/external agent
    must produce for the "paste a plan" import path — see docs/prompts or the frontend's
    copyable prompt template for the human-readable version of this schema."""
    model_config = ConfigDict(populate_by_name=True)

    project: ProjectPlanProject
    items: list[PlanItem] = Field(default_factory=list)


class GenerateProjectPlanResponse(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    plan: ProjectPlanResult
    model_used: str = Field(..., alias="modelUsed")


