//! Wire types for the Imaige API: job handles, job rows, results, filters,
//! per-mode request bodies, and the text-generation surface.
//!
//! The request bodies mirror the API's request models 1:1 (camelCase on
//! the wire — `negativePrompt`, `imageUrl`, …). Every generation POST is an
//! async enqueue that returns `202 {jobId, mode, estimatedCostCents}`;
//! callers then poll `GET /v1/jobs/{id}`, stream `GET /v1/jobs/{id}/events`,
//! or use a `*_and_wait` convenience.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// The processing mode of a job. The string values are the exact `mode`
/// tokens the server persists on the jobs row.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Mode {
    #[serde(rename = "txt2img")]
    Txt2Img,
    #[serde(rename = "img2img")]
    Img2Img,
    #[serde(rename = "inpaint")]
    Inpaint,
    #[serde(rename = "combine")]
    Combine,
    #[serde(rename = "upscale")]
    Upscale,
    #[serde(rename = "resize")]
    Resize,
    #[serde(rename = "rembg")]
    RemoveBackground,
    #[serde(rename = "analyze")]
    Analyze,
    #[serde(rename = "segment")]
    Segment,
    #[serde(rename = "smart_inpaint")]
    SmartInpaint,
    #[serde(rename = "svg_trace")]
    SvgTrace,
    #[serde(rename = "text_overlay")]
    TextOverlay,
    #[serde(rename = "ui_element")]
    UiElement,
    #[serde(rename = "animate")]
    Animate,
    #[serde(rename = "cinemagraph")]
    Cinemagraph,
    #[serde(rename = "slideshow")]
    Slideshow,
    #[serde(rename = "brand_analyze")]
    BrandAnalyze,
    #[serde(rename = "brand_generate")]
    BrandGenerate,
    #[serde(rename = "brand_export")]
    BrandExport,
    #[serde(rename = "psd_import")]
    PsdImport,
    #[serde(rename = "psd_classify")]
    PsdClassify,
    #[serde(rename = "psd_edit")]
    PsdEdit,
    #[serde(rename = "psd_export")]
    PsdExport,
    #[serde(rename = "gif_export")]
    GifExport,
    #[serde(rename = "svg_export")]
    SvgExport,
    #[serde(rename = "text_generate")]
    TextGenerate,
}

impl Mode {
    /// The route path segment the mode maps to under `/v1/images`,
    /// `/v1/videos`, `/v1/brands`, or `/v1/psd`. Returns the full path
    /// (without the base URL).
    pub fn route(self) -> &'static str {
        match self {
            Mode::Txt2Img => "/v1/images/generate",
            Mode::Img2Img => "/v1/images/transform",
            Mode::Inpaint => "/v1/images/inpaint",
            Mode::Combine => "/v1/images/combine",
            Mode::Upscale => "/v1/images/upscale",
            Mode::Resize => "/v1/images/resize",
            Mode::RemoveBackground => "/v1/images/remove-background",
            Mode::Analyze => "/v1/images/analyze",
            Mode::Segment => "/v1/images/segment",
            Mode::SmartInpaint => "/v1/images/smart-inpaint",
            Mode::SvgTrace => "/v1/images/svg-trace",
            Mode::TextOverlay => "/v1/images/text-overlay",
            Mode::UiElement => "/v1/images/ui-element",
            Mode::Animate => "/v1/videos/animate",
            Mode::Cinemagraph => "/v1/videos/cinemagraph",
            Mode::Slideshow => "/v1/videos/slideshow",
            Mode::BrandAnalyze => "/v1/brands/analyze",
            Mode::BrandGenerate => "/v1/brands/generate",
            Mode::BrandExport => "/v1/brands/export",
            Mode::PsdImport => "/v1/psd/import",
            Mode::PsdClassify => "/v1/psd/classify",
            Mode::PsdEdit => "/v1/psd/edit",
            Mode::PsdExport => "/v1/psd/export",
            Mode::GifExport => "/v1/psd/gif-export",
            Mode::SvgExport => "/v1/psd/svg-export",
            Mode::TextGenerate => "/v1/text/generate",
        }
    }
}

/// Lifecycle status of a job row.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum JobStatus {
    /// Enqueued, not yet picked up by a worker.
    Pending,
    /// A worker is actively processing it.
    Processing,
    /// Finished successfully; `mediaFileIds` / `result` are populated.
    Complete,
    /// Finished with a failure; `error` is populated.
    Error,
}

impl JobStatus {
    /// Whether this status is terminal (no further transitions expected).
    pub fn is_terminal(self) -> bool {
        matches!(self, JobStatus::Complete | JobStatus::Error)
    }
}

/// The lightweight handle returned by every generation enqueue
/// (`202 {jobId, mode, estimatedCostCents}`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobHandle {
    /// Server-assigned job id; the key for polling, streaming, and lookup.
    #[serde(rename = "jobId")]
    pub job_id: String,
    /// The mode the job was enqueued under, echoed by the server.
    pub mode: String,
    /// The server's pre-flight cost estimate, in US cents.
    #[serde(rename = "estimatedCostCents", default)]
    pub estimated_cost_cents: Option<i64>,
}

/// A full job row as returned by `GET /v1/jobs/{id}` and `GET /v1/jobs`.
///
/// Fields beyond the documented core are preserved in [`JobRow::extra`] so
/// forward-compatible additions are never dropped.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobRow {
    /// The job id (`$id` on the underlying row, normalized to `jobId`).
    #[serde(rename = "jobId", alias = "$id", alias = "id")]
    pub job_id: String,
    /// Current lifecycle status.
    pub status: JobStatus,
    /// The processing mode token.
    #[serde(default)]
    pub mode: Option<String>,
    /// IDs of the media files produced (resolve to durable URLs via
    /// [`media_url`](crate::media_url)).
    #[serde(rename = "mediaFileIds", default)]
    pub media_file_ids: Vec<String>,
    /// Per-file access tokens for the produced media. Each entry is a JSON
    /// string `{"mediaId":"…","fileToken":"…"}` matching a file id by its
    /// `mediaId` (NOT by position). Threaded into the durable URLs so they
    /// are directly fetchable.
    #[serde(rename = "mediaFileTokens", default)]
    pub media_file_tokens: Vec<String>,
    /// Free-form structured result payload (analysis dicts, brand profiles,
    /// text output, etc.) as raw JSON.
    #[serde(default)]
    pub result: Option<serde_json::Value>,
    /// Actual cost charged for this job, in US cents, once known.
    #[serde(rename = "actualCostCents", default)]
    pub actual_cost_cents: Option<i64>,
    /// The model the job ran against, if recorded.
    #[serde(default)]
    pub model: Option<String>,
    /// Error message when `status == Error`.
    #[serde(default)]
    pub error: Option<String>,
    /// Any additional server fields not modeled above.
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

/// The resolved outcome of a job — what `wait_for_job` / `*_and_wait`
/// return. Durable media URLs are pre-resolved from the file IDs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobResult {
    /// The completed job id.
    pub job_id: String,
    /// The mode token.
    pub mode: Option<String>,
    /// Terminal status (`Complete` or `Error`).
    pub status: JobStatus,
    /// Durable, 1-year-immutable media URLs for every produced file.
    pub media_urls: Vec<String>,
    /// The raw media file IDs (in case the caller wants a different host).
    pub media_file_ids: Vec<String>,
    /// The raw per-file access tokens (each a JSON string
    /// `{"mediaId":"…","fileToken":"…"}`), as returned by the server — already
    /// threaded into [`media_urls`](Self::media_urls) by `mediaId`.
    #[serde(default)]
    pub media_file_tokens: Vec<String>,
    /// Structured result payload, if any.
    pub result: Option<serde_json::Value>,
    /// Actual cost in US cents, if known.
    pub actual_cost_cents: Option<i64>,
    /// Error message if the job ended in `Error`.
    pub error: Option<String>,
}

/// Filter for `GET /v1/jobs` (list scoped to the calling key).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct JobFilter {
    /// Restrict to a non-unique customer tag (the `X-Imaige-Customer-Id`
    /// attribution value used at enqueue time).
    pub customer_id: Option<String>,
    /// Restrict to a single status.
    pub status: Option<JobStatus>,
    /// Restrict to a single mode token.
    pub mode: Option<String>,
    /// Lower bound on creation time (ISO-8601 / RFC-3339).
    pub from: Option<String>,
    /// Upper bound on creation time (ISO-8601 / RFC-3339).
    pub to: Option<String>,
    /// Page size.
    pub limit: Option<u32>,
    /// Opaque pagination cursor (the `nextCursor` from a prior page).
    pub cursor: Option<String>,
}

/// One page of a `GET /v1/jobs` listing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobList {
    /// The rows on this page.
    #[serde(alias = "items")]
    pub jobs: Vec<JobRow>,
    /// Cursor to pass as `JobFilter.cursor` for the next page, if any.
    #[serde(rename = "nextCursor", default)]
    pub next_cursor: Option<String>,
}

/// A model entry from `GET /v1/models`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
    /// The model id used in request bodies.
    #[serde(alias = "$id", alias = "modelId")]
    pub id: String,
    /// Display name, if provided.
    #[serde(default)]
    pub name: Option<String>,
    /// Any additional model metadata the server returns.
    #[serde(flatten)]
    pub extra: HashMap<String, serde_json::Value>,
}

/// A progressive event parsed from a job's SSE stream
/// (`GET /v1/jobs/{id}/events`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobEvent {
    /// The job this event belongs to.
    pub job_id: String,
    /// Current status reported by the event, if present.
    #[serde(default)]
    pub status: Option<JobStatus>,
    /// Coarse stage label (e.g. `analyze`, `inpaint`, `animate`), if any.
    #[serde(default)]
    pub stage: Option<String>,
    /// Fractional progress in `[0.0, 1.0]`, if reported.
    #[serde(default)]
    pub progress: Option<f64>,
    /// On a terminal completion event, the durable media URLs.
    #[serde(default)]
    pub media_urls: Vec<String>,
    /// On a terminal completion event, the structured result payload.
    #[serde(default)]
    pub result: Option<serde_json::Value>,
    /// Set when the event signals failure.
    #[serde(default)]
    pub error: Option<String>,
    /// Whether this is the terminal event of the stream.
    #[serde(default)]
    pub terminal: bool,
}

// ---------------------------------------------------------------------------
// Per-mode request bodies. `attribution` is stripped before serialization
// and applied as headers; everything else maps directly to the JSON body.
// ---------------------------------------------------------------------------

/// Optional per-request attribution applied as HTTP headers, not body.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Attribution {
    /// `Idempotency-Key` — a unique value to make the enqueue retry-safe.
    pub idempotency_key: Option<String>,
    /// `X-Imaige-Customer-Id` — a non-unique tag for grouping/filtering.
    pub customer_id: Option<String>,
}

/// Text-to-image generation (`POST /v1/images/generate`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateImageRequest {
    /// The text prompt.
    pub prompt: String,
    /// Model id (e.g. `example-model` — your chosen model).
    pub model: String,
    /// Negative prompt; empty string means "none".
    #[serde(
        rename = "negativePrompt",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub negative_prompt: Option<String>,
    /// Preset size (`square_hd`, `landscape_16_9`, …).
    #[serde(rename = "imageSize", default, skip_serializing_if = "Option::is_none")]
    pub image_size: Option<String>,
    /// Number of images to produce (1–8).
    #[serde(rename = "numImages", default, skip_serializing_if = "Option::is_none")]
    pub num_images: Option<u32>,
    /// Optional deterministic seed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub seed: Option<i64>,
    /// Attribution headers (not serialized into the body).
    #[serde(skip)]
    pub attribution: Attribution,
}

/// Image-to-image transform (`POST /v1/images/transform`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransformImageRequest {
    /// The text prompt.
    pub prompt: String,
    /// Model id.
    pub model: String,
    /// Source image URL.
    #[serde(rename = "imageUrl")]
    pub image_url: String,
    /// Transform strength (0.0–1.0).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub strength: Option<f64>,
    /// Negative prompt.
    #[serde(
        rename = "negativePrompt",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub negative_prompt: Option<String>,
    /// Optional mask (presence routes server-side to inpaint).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mask: Option<String>,
    /// Preset size.
    #[serde(rename = "imageSize", default, skip_serializing_if = "Option::is_none")]
    pub image_size: Option<String>,
    /// Number of images (1–8).
    #[serde(rename = "numImages", default, skip_serializing_if = "Option::is_none")]
    pub num_images: Option<u32>,
    /// Optional seed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub seed: Option<i64>,
    /// Attribution headers.
    #[serde(skip)]
    pub attribution: Attribution,
}

/// Multi-reference image compose (`POST /v1/images/combine`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombineImagesRequest {
    /// Model id.
    pub model: String,
    /// The text prompt describing how to compose the references.
    pub prompt: String,
    /// Reference image URLs to compose.
    #[serde(rename = "imageUrls")]
    pub image_urls: Vec<String>,
    /// Preset size.
    #[serde(rename = "imageSize", default, skip_serializing_if = "Option::is_none")]
    pub image_size: Option<String>,
    /// Optional seed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub seed: Option<i64>,
    /// Attribution headers (not serialized into the body).
    #[serde(skip)]
    pub attribution: Attribution,
}

/// A labeled character/reference for a composite scene.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CharacterRef {
    /// Reference image URL.
    pub url: String,
    /// The label/name for this reference in the composited scene.
    pub name: String,
}

/// Composite-scene compose (`POST /v1/images/combine-composite`).
///
/// The server composites a labeled background + character references, then
/// runs a fast img2img pass over the composite.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombineCompositeRequest {
    /// Model id.
    pub model: String,
    /// The text prompt describing the final scene.
    pub prompt: String,
    /// Background image URL.
    #[serde(rename = "backgroundUrl")]
    pub background_url: String,
    /// Labeled character references to composite over the background.
    pub characters: Vec<CharacterRef>,
    /// Preset size.
    #[serde(rename = "imageSize", default, skip_serializing_if = "Option::is_none")]
    pub image_size: Option<String>,
    /// Optional seed.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub seed: Option<i64>,
    /// img2img strength (0.0–1.0) applied over the composite.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub strength: Option<f64>,
    /// Attribution headers (not serialized into the body).
    #[serde(skip)]
    pub attribution: Attribution,
}

/// AI upscale (`POST /v1/images/upscale`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpscaleImageRequest {
    /// Source image URL.
    #[serde(rename = "imageUrl")]
    pub image_url: String,
    /// Upscale model id.
    #[serde(
        rename = "upscaleModel",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub upscale_model: Option<String>,
    /// Integer factor (2–8).
    #[serde(
        rename = "upscaleFactor",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub upscale_factor: Option<u32>,
    /// Target resolution (`"2K"`, `"4K"`), mutually exclusive with factor on
    /// some models.
    #[serde(
        rename = "targetResolution",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub target_resolution: Option<String>,
    /// Attribution headers.
    #[serde(skip)]
    pub attribution: Attribution,
}

/// Background removal (`POST /v1/images/remove-background`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoveBackgroundRequest {
    /// Source image URL.
    #[serde(rename = "imageUrl")]
    pub image_url: String,
    /// Attribution headers.
    #[serde(skip)]
    pub attribution: Attribution,
}

/// VLM scene analysis (`POST /v1/images/analyze`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyzeImageRequest {
    /// Source image URL.
    #[serde(rename = "imageUrl")]
    pub image_url: String,
    /// Attribution headers.
    #[serde(skip)]
    pub attribution: Attribution,
}

/// Image-to-video animation (`POST /v1/videos/animate`).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimateVideoRequest {
    /// Source image URL.
    #[serde(rename = "imageUrl")]
    pub image_url: String,
    /// Motion prompt.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt: Option<String>,
    /// Video backend (`kling`, `minimax`, `luma`, … or `auto`).
    #[serde(
        rename = "videoBackend",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub video_backend: Option<String>,
    /// Clip duration in seconds (1.0–30.0).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration: Option<f64>,
    /// Attribution headers.
    #[serde(skip)]
    pub attribution: Attribution,
}

/// One chat message for the text-generation surface.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMessage {
    /// Role: `system`, `user`, or `assistant`.
    pub role: String,
    /// Message content.
    pub content: String,
}

/// Text generation (`POST /v1/text/generate`).
///
/// Provide exactly one of `prompt` or `messages`. `system` and the sampling
/// knobs are optional.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateTextRequest {
    /// Required model id.
    pub model: String,
    /// Single-turn prompt (mutually exclusive with `messages`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt: Option<String>,
    /// Multi-turn message list (mutually exclusive with `prompt`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub messages: Option<Vec<ChatMessage>>,
    /// Optional system prompt.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub system: Option<String>,
    /// Cap on generated tokens.
    #[serde(
        rename = "max_tokens",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub max_tokens: Option<u32>,
    /// Sampling temperature.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub temperature: Option<f64>,
    /// Optional structured-output format spec (raw JSON, e.g. a JSON-schema
    /// `response_format` object).
    #[serde(
        rename = "response_format",
        default,
        skip_serializing_if = "Option::is_none"
    )]
    pub response_format: Option<serde_json::Value>,
    /// Attribution headers (not serialized into the body).
    #[serde(skip)]
    pub attribution: Attribution,
}

/// Token-usage accounting from a synchronous text generation.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TextUsage {
    /// Prompt tokens consumed.
    #[serde(rename = "promptTokens", alias = "prompt_tokens", default)]
    pub prompt_tokens: Option<u32>,
    /// Completion tokens produced.
    #[serde(rename = "completionTokens", alias = "completion_tokens", default)]
    pub completion_tokens: Option<u32>,
    /// Total tokens (`prompt + completion`).
    #[serde(rename = "totalTokens", alias = "total_tokens", default)]
    pub total_tokens: Option<u32>,
}

/// The resolved output of `generate_text_sync`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextResult {
    /// The generated text.
    pub text: String,
    /// The model that produced it.
    pub model: String,
    /// Token usage, if the server reported it.
    pub usage: TextUsage,
}
