use std::str::FromStr;

use crate::{RsRequest, url::RsLink};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use strum_macros::EnumString;

use crate::domain::backup::BackupFile;

pub const DEFAULT_MIME: &str = "application/octet-stream";

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FileEpisode {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub season: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub episode: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub episode_to: Option<u32>,
}

impl FromStr for FileEpisode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let splitted: Vec<&str> = s.split("|").collect();
        if splitted.len() == 3 {
            Ok(FileEpisode {
                id: splitted[0].to_string(),
                season: splitted[1].parse::<u32>().ok().and_then(|i| {
                    if i == 0 {
                        None
                    } else {
                        Some(i)
                    }
                }),
                episode: splitted[2].parse::<u32>().ok().and_then(|i| {
                    if i == 0 {
                        None
                    } else {
                        Some(i)
                    }
                }),
                episode_to: None,
            })
        } else if splitted.len() == 2 {
            Ok(FileEpisode {
                id: splitted[0].to_string(),
                season: splitted[1].parse::<u32>().ok().and_then(|i| {
                    if i == 0 {
                        None
                    } else {
                        Some(i)
                    }
                }),
                episode: None,
                episode_to: None,
            })
        } else {
            Ok(FileEpisode {
                id: splitted[0].to_string(),
                season: None,
                episode: None,
                episode_to: None,
            })
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct MediaItemReference {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conf: Option<u16>,
}

impl FromStr for MediaItemReference {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let splitted: Vec<&str> = s.split('|').collect();
        if splitted.len() == 2 {
            Ok(MediaItemReference {
                id: splitted[0].to_string(),
                conf: splitted[1].parse::<u16>().ok().and_then(|e| {
                    if e == 100 {
                        None
                    } else {
                        Some(e)
                    }
                }),
            })
        } else {
            Ok(MediaItemReference {
                id: splitted[0].to_string(),
                conf: None,
            })
        }
    }
}

#[derive(
    Debug, Serialize, Deserialize, Clone, PartialEq, strum_macros::Display, EnumString, Default,
)]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub enum FileType {
    Directory,
    Photo,
    Video,
    Archive,
    Album,
    Book,
    #[default]
    Other,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Media {
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
    pub name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    #[serde(rename = "type")]
    pub kind: FileType,
    pub mimetype: String,
    pub size: Option<u64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub params: Option<Value>,

    pub added: Option<i64>,
    pub modified: Option<i64>,
    pub created: Option<i64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub rating: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub avg_rating: Option<f32>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub md5: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub width: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub height: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub phash: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thumbhash: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub focal: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iso: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub color_space: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub icc: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mp: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sspeed: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub f_number: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub orientation: Option<usize>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub acodecs: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub achan: Option<Vec<usize>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub vcodecs: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fps: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bitrate: Option<u64>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub long: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lat: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub pages: Option<usize>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub progress: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub faces: Option<Vec<FaceEmbedding>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub backups: Option<Vec<BackupFile>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thumb: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thumbv: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub thumbsize: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub iv: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub origin: Option<RsLink>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lang: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uploader: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub uploadkey: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub original_hash: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub original_id: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub face_recognition_error: Option<String>,
}

impl Media {
    pub fn max_date(&self) -> i64 {
        *[
            self.created.unwrap_or(0),
            self.added.unwrap_or(0),
            self.modified.unwrap_or(0),
        ]
        .iter()
        .max()
        .unwrap_or(&0)
    }

    pub fn bytes_size(&self) -> Option<u64> {
        if self.iv.is_none() {
            self.size
        } else {
            //16 Bytes to store IV
            //4 to store encrypted thumb size = T (can be 0)
            //4 to store encrypted Info size = I (can be 0)
            //32 to store thumb mimetype
            //256 to store file mimetype
            //T Bytes for the encrypted thumb
            //I Bytes for the encrypted info
            if let Some(file_size) = self.size {
                Some(file_size + 16 + 4 + 4 + 32 + 256 + self.thumbsize.unwrap_or(0) + 0)
            } else {
                None
            }
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct RsGpsPosition {
    pub lat: f64,
    pub long: f64,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FaceEmbedding {
    pub id: String,
    pub embedding: Vec<f32>,
    pub media_ref: Option<String>,
    pub bbox: Option<FaceBBox>,
    pub confidence: Option<f32>,
    pub pose: Option<(f32, f32, f32)>,
    pub person_id: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FaceBBox {
    pub x1: f32,
    pub y1: f32,
    pub x2: f32,
    pub y2: f32,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub video_s: Option<f32>, // Seconds in video where face was detected
    #[serde(skip_serializing_if = "Option::is_none")]
    pub video_percent: Option<u32>, // Percent (0-100) of video where face was detected
}







#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MediaForUpdate {
    pub name: Option<String>,
    pub description: Option<String>,
    pub mimetype: Option<String>,
    pub kind: Option<FileType>,
    pub size: Option<u64>,

    pub md5: Option<String>,

    pub modified: Option<i64>,
    pub created: Option<i64>,

    pub width: Option<u32>,
    pub height: Option<u32>,
    pub orientation: Option<u8>,
    pub color_space: Option<String>,
    pub icc: Option<String>,
    pub mp: Option<u32>,
    pub vcodecs: Option<Vec<String>>,
    pub acodecs: Option<Vec<String>>,
    pub fps: Option<f64>,
    pub bitrate: Option<u64>,
    pub focal: Option<u64>,
    pub iso: Option<u64>,
    pub model: Option<String>,
    pub sspeed: Option<String>,
    pub f_number: Option<f64>,

    pub pages: Option<usize>,

    pub duration: Option<u64>,

    pub progress: Option<usize>,

    pub add_tags: Option<Vec<MediaItemReference>>,
    pub remove_tags: Option<Vec<String>>,
    pub tags_lookup: Option<Vec<String>>,

    pub add_series: Option<Vec<FileEpisode>>,
    pub remove_series: Option<Vec<FileEpisode>>,
    pub series_lookup: Option<Vec<String>>,
    pub season: Option<u32>,
    pub episode: Option<u32>,

    pub add_people: Option<Vec<MediaItemReference>>,
    pub remove_people: Option<Vec<String>>,
    pub people_lookup: Option<Vec<String>>,

    pub long: Option<f64>,
    pub lat: Option<f64>,
    pub gps: Option<String>,

    pub origin: Option<RsLink>,
    pub origin_url: Option<String>,
    #[serde(default)]
    pub ignore_origin_duplicate: bool,

    pub movie: Option<String>,
    pub book: Option<String>,

    pub lang: Option<String>,

    pub rating: Option<u16>,

    pub thumbsize: Option<usize>,
    pub iv: Option<String>,

    pub uploader: Option<String>,
    pub uploadkey: Option<String>,
    pub upload_id: Option<String>,

    pub original_hash: Option<String>,
    pub original_id: Option<String>,
}


impl MediaForUpdate {
    /// Merge `patch` into `self`.
    ///
    /// Semantics:
    /// - For most `Option<T>` fields: overwrite only when `patch.field` is `Some`.
    /// - For "list-like update" fields (`add_*`, `remove_*`, `*_lookup`): append vectors.
    /// - For `ignore_origin_duplicate` (bool): OR (once true, stays true).
    pub fn merge_from(&mut self, mut patch: Self) {
        // Overwrite `dst` only if `src` is Some(..)
        fn overwrite_if_some<T>(dst: &mut Option<T>, src: &mut Option<T>) {
            if src.is_some() {
                *dst = src.take();
            }
        }

        // Append vectors when both sides are Some(vec).
        // If `dst` is None, it becomes src (if any).
        fn append_vec<T>(dst: &mut Option<Vec<T>>, src: &mut Option<Vec<T>>) {
            match (dst.as_mut(), src.take()) {
                (Some(d), Some(mut s)) => d.append(&mut s),
                (None, Some(s)) => *dst = Some(s),
                _ => {}
            }
        }

        // ----- Scalar / metadata fields (overwrite-if-some) -----
        overwrite_if_some(&mut self.name, &mut patch.name);
        overwrite_if_some(&mut self.description, &mut patch.description);
        overwrite_if_some(&mut self.mimetype, &mut patch.mimetype);
        overwrite_if_some(&mut self.kind, &mut patch.kind);
        overwrite_if_some(&mut self.size, &mut patch.size);

        overwrite_if_some(&mut self.md5, &mut patch.md5);

        overwrite_if_some(&mut self.modified, &mut patch.modified);
        overwrite_if_some(&mut self.created, &mut patch.created);

        overwrite_if_some(&mut self.width, &mut patch.width);
        overwrite_if_some(&mut self.height, &mut patch.height);
        overwrite_if_some(&mut self.orientation, &mut patch.orientation);
        overwrite_if_some(&mut self.color_space, &mut patch.color_space);
        overwrite_if_some(&mut self.icc, &mut patch.icc);
        overwrite_if_some(&mut self.mp, &mut patch.mp);
        overwrite_if_some(&mut self.vcodecs, &mut patch.vcodecs);
        overwrite_if_some(&mut self.acodecs, &mut patch.acodecs);
        overwrite_if_some(&mut self.fps, &mut patch.fps);
        overwrite_if_some(&mut self.bitrate, &mut patch.bitrate);
        overwrite_if_some(&mut self.focal, &mut patch.focal);
        overwrite_if_some(&mut self.iso, &mut patch.iso);
        overwrite_if_some(&mut self.model, &mut patch.model);
        overwrite_if_some(&mut self.sspeed, &mut patch.sspeed);
        overwrite_if_some(&mut self.f_number, &mut patch.f_number);

        overwrite_if_some(&mut self.pages, &mut patch.pages);
        overwrite_if_some(&mut self.duration, &mut patch.duration);
        overwrite_if_some(&mut self.progress, &mut patch.progress);

        overwrite_if_some(&mut self.season, &mut patch.season);
        overwrite_if_some(&mut self.episode, &mut patch.episode);

        overwrite_if_some(&mut self.long, &mut patch.long);
        overwrite_if_some(&mut self.lat, &mut patch.lat);
        overwrite_if_some(&mut self.gps, &mut patch.gps);

        overwrite_if_some(&mut self.origin, &mut patch.origin);
        overwrite_if_some(&mut self.origin_url, &mut patch.origin_url);

        overwrite_if_some(&mut self.movie, &mut patch.movie);
        overwrite_if_some(&mut self.book, &mut patch.book);

        overwrite_if_some(&mut self.lang, &mut patch.lang);
        overwrite_if_some(&mut self.rating, &mut patch.rating);

        overwrite_if_some(&mut self.thumbsize, &mut patch.thumbsize);
        overwrite_if_some(&mut self.iv, &mut patch.iv);

        overwrite_if_some(&mut self.uploader, &mut patch.uploader);
        overwrite_if_some(&mut self.uploadkey, &mut patch.uploadkey);
        overwrite_if_some(&mut self.upload_id, &mut patch.upload_id);

        overwrite_if_some(&mut self.original_hash, &mut patch.original_hash);
        overwrite_if_some(&mut self.original_id, &mut patch.original_id);

        // ----- List-like update fields (append) -----
        append_vec(&mut self.add_tags, &mut patch.add_tags);
        append_vec(&mut self.remove_tags, &mut patch.remove_tags);
        append_vec(&mut self.tags_lookup, &mut patch.tags_lookup);

        append_vec(&mut self.add_series, &mut patch.add_series);
        append_vec(&mut self.remove_series, &mut patch.remove_series);
        append_vec(&mut self.series_lookup, &mut patch.series_lookup);

        append_vec(&mut self.add_people, &mut patch.add_people);
        append_vec(&mut self.remove_people, &mut patch.remove_people);
        append_vec(&mut self.people_lookup, &mut patch.people_lookup);

        // ----- Non-Option field -----
        self.ignore_origin_duplicate |= patch.ignore_origin_duplicate;
    }

    /// Convenience: returns a merged value without mutating the original.
    pub fn merged(mut self, patch: Self) -> Self {
        self.merge_from(patch);
        self
    }
}


impl From<Media> for MediaForUpdate {
    fn from(value: Media) -> Self {
        MediaForUpdate {
            description: value.description,
            add_people: None,
            add_tags: None,
            long: value.long,
            lat: value.lat,
            created: value.created,
            origin: value.origin,
            add_series: None,
            pages: value.pages,
            original_hash: value.original_hash.or(value.md5),
            original_id: Some(value.original_id.unwrap_or(value.id)),
            book: None,
            ..Default::default()
        }
    }
}

impl From<RsRequest> for MediaForUpdate {
    fn from(value: RsRequest) -> Self {
        // Build add_series from albums (first entry) + season + episode if albums are provided
        let (add_series, season, episode) = if let Some(albums) = &value.albums {
            if let Some(serie_id) = albums.first() {
                // Have direct serie ID - use add_series, don't need season/episode at top level
                (
                    Some(vec![FileEpisode {
                        id: serie_id.clone(),
                        season: value.season,
                        episode: value.episode,
                        episode_to: None,
                    }]),
                    None,
                    None,
                )
            } else {
                (None, value.season, value.episode)
            }
        } else {
            // No direct album IDs - keep season/episode for series_lookup pairing
            (None, value.season, value.episode)
        };

        MediaForUpdate {
            name: value.filename_or_extract_from_url(),
            mimetype: value.mime,
            description: value.description,
            ignore_origin_duplicate: value.ignore_origin_duplicate,
            size: value.size,
            // Use the new lookup fields for database text search
            people_lookup: value.people_lookup,
            tags_lookup: value.tags_lookup,
            series_lookup: value.albums_lookup,
            add_series,
            movie: value.movie,
            season,
            episode,
            ..Default::default()
        }
    }
}

