use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Stored as text in Postgres; decoded via TryFrom so an unknown value is a
/// loud decode error instead of a silently misbehaving string.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum PhotoStatus {
    Uploaded,
    Processing,
    Ready,
    Error,
}

impl PhotoStatus {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Uploaded => "uploaded",
            Self::Processing => "processing",
            Self::Ready => "ready",
            Self::Error => "error",
        }
    }
}

impl std::str::FromStr for PhotoStatus {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, String> {
        match value {
            "uploaded" => Ok(Self::Uploaded),
            "processing" => Ok(Self::Processing),
            "ready" => Ok(Self::Ready),
            "error" => Ok(Self::Error),
            other => Err(format!("unknown photo status: {other}")),
        }
    }
}

// #[sqlx(try_from = "String")] needs TryFrom; delegate to FromStr.
impl TryFrom<String> for PhotoStatus {
    type Error = String;

    fn try_from(value: String) -> Result<Self, String> {
        value.parse()
    }
}

/// Client accept/reject vote. Same convention as PhotoStatus: text in
/// Postgres (check-constrained), this enum everywhere Rust touches the value
/// — serde rejects anything but "accept"/"reject" at the API boundary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Verdict {
    Accept,
    Reject,
}

impl Verdict {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Accept => "accept",
            Self::Reject => "reject",
        }
    }
}

/// The one place feedback-aggregation policy lives (UI filters and XMP
/// export must agree): the rating is the MAX across everyone, any accept
/// wins over rejects, and the owner's feedback counts like a client's.
#[derive(Debug, Clone, Copy, Default, Serialize)]
pub struct FeedbackAggregate {
    pub top_rating: Option<i32>,
    pub accepts: i32,
    pub rejects: i32,
}

impl FeedbackAggregate {
    pub fn add_rating(&mut self, rating: i32) {
        self.top_rating = Some(self.top_rating.map_or(rating, |top| top.max(rating)));
    }

    pub fn add_verdict(&mut self, verdict: &str) {
        if verdict == "accept" {
            self.accepts += 1;
        } else {
            self.rejects += 1;
        }
    }

    pub fn add_owner(&mut self, rating: Option<i32>, verdict: Option<&str>) {
        if let Some(rating) = rating {
            self.add_rating(rating);
        }
        if let Some(verdict) = verdict {
            self.add_verdict(verdict);
        }
    }

    pub fn is_empty(&self) -> bool {
        self.top_rating.is_none() && self.accepts == 0 && self.rejects == 0
    }

    pub fn merge(&mut self, other: &FeedbackAggregate) {
        if let Some(rating) = other.top_rating {
            self.add_rating(rating);
        }
        self.accepts += other.accepts;
        self.rejects += other.rejects;
    }

    /// XMP color label: any accept wins, otherwise any reject.
    pub fn label(&self) -> Option<&'static str> {
        if self.accepts > 0 {
            Some("Green")
        } else if self.rejects > 0 {
            Some("Red")
        } else {
            None
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobKind {
    ProcessPhoto,
    DeleteS3Prefix,
}

impl JobKind {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::ProcessPhoto => "process_photo",
            Self::DeleteS3Prefix => "delete_s3_prefix",
        }
    }
}

impl std::str::FromStr for JobKind {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, String> {
        match value {
            "process_photo" => Ok(Self::ProcessPhoto),
            "delete_s3_prefix" => Ok(Self::DeleteS3Prefix),
            other => Err(format!("unknown job kind: {other}")),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobStatus {
    Queued,
    Running,
    Done,
    Failed,
}

impl JobStatus {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Queued => "queued",
            Self::Running => "running",
            Self::Done => "done",
            Self::Failed => "failed",
        }
    }
}

#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct Album {
    pub id: Uuid,
    pub name: String,
    pub description: String,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct Photo {
    pub id: Uuid,
    pub album_id: Uuid,
    pub filename: String,
    pub content_type: String,
    pub size_bytes: i64,
    #[sqlx(try_from = "String")]
    pub status: PhotoStatus,
    pub error: Option<String>,
    pub width: Option<i32>,
    pub height: Option<i32>,
    pub taken_at: Option<DateTime<Utc>>,
    pub processed_at: Option<DateTime<Utc>>,
    pub owner_rating: Option<i32>,
    pub owner_verdict: Option<String>,
    #[serde(skip_serializing)]
    pub sha256: Option<String>,
    #[serde(skip_serializing)]
    pub crc32: Option<i64>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
pub struct Share {
    pub id: Uuid,
    pub album_id: Uuid,
    pub token: String,
    pub label: String,
    #[serde(skip_serializing)]
    pub password_hash: Option<String>,
    pub allow_download: bool,
    pub expires_at: Option<DateTime<Utc>>,
    #[serde(skip_serializing)]
    pub failed_attempts: i32,
    #[serde(skip_serializing)]
    pub locked_until: Option<DateTime<Utc>>,
    pub created_at: DateTime<Utc>,
}
