"""Pydantic schemas for session suggested fixes (Phase 3).

See FLOWPILOT-MIGRATION.md Section 5.2.
"""
from __future__ import annotations

from datetime import datetime
from typing import Any, Literal
from uuid import UUID

from pydantic import BaseModel, Field

UserDecision = Literal["one_off", "draft_template", "build_template", "dismissed"]

# "dismissed" here is the outcome dimension — orthogonal to UserDecision's
# "dismissed" (script-path choice), though the migration backfill aligns
# them for pre-existing rows.
FixStatus = Literal[
    "proposed",
    "applied_success",
    "applied_failed",
    "applied_partial",
    "dismissed",
]


class SessionSuggestedFixResponse(BaseModel):
    id: UUID
    session_id: UUID
    title: str
    description: str
    confidence_pct: int
    script_template_id: UUID | None
    ai_drafted_script: str | None
    ai_drafted_parameters: dict[str, Any] | None
    user_decision: UserDecision | None
    superseded_at: datetime | None
    created_at: datetime
    status: FixStatus
    applied_at: datetime | None
    verified_at: datetime | None
    partial_notes: str | None
    failure_reason: str | None
    ai_outcome_proposal: dict[str, Any] | None

    model_config = {"from_attributes": True}


class SessionSuggestedFixDecisionRequest(BaseModel):
    """Engineer's path choice on a suggested fix.

    Server-side side effects per Section 5.2:
    - one_off: record decision, return the rendered (AI-drafted or
      engineer-edited) script. No persistent library artifact created.
    - draft_template: same as one_off, plus TemplateExtractionService
      proposes a parameterization and a draft_templates row is created.
    - build_template: return a redirect payload pointing at the Script
      Builder page, pre-loaded with the drafted script body.
    - dismissed: mark the fix superseded.

    For one_off / draft_template, the engineer may have edited the drafted
    script or its parameters in the dialog. The final versions are sent
    back here so we persist what will actually run.
    """
    decision: UserDecision
    # Present for one_off / draft_template — the engineer's final version of
    # the drafted script after any inline edits. Omit to use the fix's
    # `ai_drafted_script` verbatim.
    edited_script: str | None = Field(None, min_length=1, max_length=50_000)
    # Parameter values used when rendering (informational, stored on the
    # draft_template row so a reviewer can see what the first run used).
    parameters_used: dict[str, Any] | None = None


class SessionSuggestedFixDecisionResponse(BaseModel):
    """Returned after recording a decision."""
    id: UUID
    user_decision: UserDecision
    # Populated for one_off / draft_template — the script to display/run.
    rendered_script: str | None = None
    # Populated for draft_template — the ID of the draft_templates row so
    # the post-resolve TemplatizePrompt can fetch it in Phase 6.
    draft_template_id: UUID | None = None
    # Populated for build_template — where to send the engineer next.
    redirect_path: str | None = Field(
        None,
        description="Where to send the engineer next (e.g. /scripts/builder?... for build_template)",
    )


# Subset of FixStatus that the engineer can set via the outcome endpoint —
# `proposed` is excluded because you can't un-decide a fix back to "proposed".
FixOutcome = Literal[
    "applied_success", "applied_failed", "applied_partial", "dismissed"
]


class SessionSuggestedFixOutcomeRequest(BaseModel):
    """Engineer-reported outcome of applying a suggested fix.

    Writes to session_suggested_fixes.status and companion columns. This is
    orthogonal to `user_decision` (which records which script-path the
    engineer took); outcome captures whether the fix actually worked.

    Allowed transitions:
    - from `proposed` or `applied_partial`: any outcome is valid
      (partial is parked, not terminal — the engineer may update notes,
      abandon via dismiss, or advance to success/failed)
    - from any terminal outcome (`applied_success`, `applied_failed`,
      `dismissed`): server returns 409
    """
    outcome: FixOutcome
    # Required for applied_partial, optional for applied_failed, ignored otherwise.
    notes: str | None = Field(None, max_length=500)


class SessionSuggestedFixScriptRequest(BaseModel):
    """Engineer-submitted drafted script for a suggested fix.

    Called when the inline Script Builder tab's Submit action fires. The
    fix must be non-terminal (still proposed/applied_partial). Setting
    the script does NOT stamp applied_at — a draft is not an application.
    """
    ai_drafted_script: str = Field(..., min_length=1, max_length=50_000)
    ai_drafted_parameters: dict[str, Any] | None = None


# ── Resolution note preview ────────────────────────────────────────────────

class ResolutionNotePreviewResponse(BaseModel):
    markdown: str
    target_ticket_ref: str | None
    state_version: int
    from_cache: bool


# ── Phase 4: Resolve + Escalate post ───────────────────────────────────────

class ResolutionNotePostRequest(BaseModel):
    """Engineer-edited resolution markdown. Server posts to PSA + marks resolved."""
    markdown: str = Field(..., min_length=1, max_length=20_000)
    # Optional override for resolution summary shown on the session listing;
    # defaults to the first line of the markdown if omitted.
    resolution_summary: str | None = Field(None, max_length=500)


class EscalationPackagePostRequest(BaseModel):
    markdown: str = Field(..., min_length=1, max_length=20_000)
    # Free-text reason shown in session listings and escalation queue.
    escalation_reason: str | None = Field(None, max_length=500)


class ResolutionPostResponse(BaseModel):
    """Response shape for both Resolve/Escalate POST endpoints."""
    # "resolved" / "escalated" / "resolved_local" / "escalated_local"
    # The _local variants indicate the session has no linked PSA ticket —
    # markdown is stored, session state is updated, nothing was posted externally.
    outcome: str
    session_status: str
    external_id: str | None = None
    posted_at: datetime | None = None
    # Populated when a status transition was attempted and verified. None
    # when no target status is configured in account_settings.preferences.
    verified_status_id: int | None = None
    verified_status_name: str | None = None
    status_transition_skipped_reason: str | None = None
