use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use uuid::Uuid;

// A strict representation of the Studio project format shown in the sample.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StudioProject {
    pub state: State,
    pub id: Uuid,
    pub title: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub archived: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct State {
    #[serde(rename = "loop")] // raw ident is fine, but be explicit
    pub r#loop: LoopRegion,
    pub timing: Timing,
    pub tracks: Vec<Track>,
    pub metronome: Metronome,
    pub selection: Selection,
    // Some environments include a global amplitude on state.
    #[serde(default = "default_one")]
    pub amplitude: f64,
    #[serde(rename = "markersRegistry")]
    pub markers_registry: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
    #[serde(rename = "songFadeInBeats")]
    pub song_fade_in_beats: f64,
    #[serde(rename = "songFadeOutBeats")]
    pub song_fade_out_beats: f64,
    // Unknown shape; keep as empty map of arbitrary JSON values for now.
    #[serde(rename = "lyricsCorrectionsByClipId")]
    pub lyrics_corrections_by_clip_id: BTreeMap<Uuid, serde_json::Value>,
    // Metadata containing flags like usedDelete
    #[serde(default)]
    pub metadata: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LoopRegion {
    pub enabled: bool,
    #[serde(rename = "endBeats")]
    pub end_beats: f64,
    #[serde(rename = "startBeats")]
    pub start_beats: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Timing {
    #[serde(rename = "type")]
    pub kind: TimingType,
    #[serde(rename = "trackId")]
    pub track_id: Uuid,
    #[serde(rename = "fallbackBPS")]
    pub fallback_bps: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TimingType {
    FollowTrack,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Track {
    pub id: Uuid,
    pub arm: bool,
    pub icon: TrackIcon,
    pub mute: bool,
    pub name: String,
    pub solo: bool,
    pub clips: Vec<Clip>,
    pub color: HexColor,
    pub height: f64,
    pub balance: f64,
    pub amplitude: f64,
    // Present on newer API payloads; absent on older ones
    pub instrument: Option<TrackInstrument>,
    #[serde(rename = "takeLanes")]
    pub take_lanes: Vec<serde_json::Value>,
    #[serde(rename = "takeLanesExpanded")]
    pub take_lanes_expanded: bool,
    #[serde(rename = "clipCreationIntents")]
    pub clip_creation_intents: Vec<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum TrackIcon {
    Audio,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Clip {
    pub id: Uuid,
    #[serde(rename = "loop")] // raw ident is fine, but be explicit
    pub r#loop: LoopRegion,
    pub name: String,
    #[serde(default)]
    pub mute: bool,
    pub warp: Warp,
    pub color: HexColor,
    #[serde(rename = "clipId")]
    pub clip_id: Uuid,
    #[serde(rename = "endBeats")]
    pub end_beats: f64,
    pub amplitude: f64,
    pub streaming: bool,
    #[serde(rename = "startBeats")]
    pub start_beats: f64,
    #[serde(rename = "fadeInBeats")]
    pub fade_in_beats: f64,
    #[serde(rename = "fadeOutBeats")]
    pub fade_out_beats: f64,
    pub transposition: f64,
    #[serde(rename = "readStartBeats")]
    pub read_start_beats: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Warp {
    pub speed: f64,
    pub enabled: bool,
    #[serde(rename = "markersHash")]
    pub markers_hash: String,
    #[serde(rename = "awaitingAnalysis")]
    pub awaiting_analysis: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Metronome {
    pub enabled: bool,
    pub amplitude: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Selection {
    #[serde(rename = "trackIds")]
    pub track_ids: Vec<Uuid>,
    #[serde(rename = "focusBeats")]
    pub focus_beats: f64,
    #[serde(rename = "anchorBeats")]
    pub anchor_beats: f64,
    #[serde(rename = "focusedArea")]
    pub focused_area: FocusedArea,
    #[serde(rename = "focusedTrackId")]
    pub focused_track_id: Uuid,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FocusedArea {
    Timeline,
}

// Strict hex color in the form #RRGGBB
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct HexColor(pub String);

impl<'de> Deserialize<'de> for HexColor {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        validate_hex_color(&s).map_err(serde::de::Error::custom)?;
        Ok(HexColor(s))
    }
}

impl Serialize for HexColor {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.0)
    }
}

fn validate_hex_color(s: &str) -> Result<(), &'static str> {
    if s.len() != 7 || !s.starts_with('#') {
        return Err("color must be in form #RRGGBB");
    }
    if s.as_bytes()[1..]
        .iter()
        .all(|b| matches!(*b as char, '0'..='9' | 'a'..='f' | 'A'..='F'))
    {
        Ok(())
    } else {
        Err("color must contain only hex digits")
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TrackInstrument {
    #[serde(rename = "type")]
    pub kind: TrackInstrumentType,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrackInstrumentType {
    Song,
}

fn default_one() -> f64 {
    1.0
}
