//! Media parsing primitives and filename/NFO parsing helpers.

#![warn(missing_docs)]

use std::fs::read_to_string;
use std::path::Path;
use std::sync::LazyLock;

use mediarium_macros::string_enum;
use regex::Regex;
use salvo::oapi::ToSchema;
#[cfg(feature = "db")]
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

/// Shared media type classification used across parsing and storage layers.
#[string_enum]
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, ToSchema)]
#[cfg_attr(feature = "db", derive(DeriveActiveEnum, EnumIter))]
#[cfg_attr(feature = "db", sea_orm(rs_type = "String", db_type = "Text"))]
#[serde(rename_all = "lowercase")]
pub enum MediaType {
    /// A movie or movie-like standalone item.
    #[default]
    Movie,
    /// A series or episodic item.
    Series,
}

/// Parsed media identity extracted from a filename or nearby NFO file.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MediaInfo {
    /// Canonical or best-effort title.
    pub title: String,
    /// Optional release year.
    pub year: Option<i32>,
    /// Optional season number for episodic content.
    pub season: Option<i32>,
    /// Optional episode number for episodic content.
    pub episode: Option<i32>,
    /// Parsed media type classification.
    pub media_type: MediaType,
}

static EPISODE_RE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?i)[Ss](\d{1,2})[Ee](\d{1,3})").unwrap());
static YEAR_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[\.\(\s](\d{4})[\.\)\s]").unwrap());
static CLEAN_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[\.\-_\(\)\[\]]").unwrap());

/// Parse a media file path into a [`MediaInfo`] value.
pub fn parse_filename(path: &Path) -> MediaInfo {
    let mut info = MediaInfo::default();

    if let Some(nfo_info) = try_parse_nfo(path) {
        return nfo_info;
    }

    let file_stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_string();

    if let Some(caps) = EPISODE_RE.captures(&file_stem) {
        info.season = caps.get(1).and_then(|m| m.as_str().parse().ok());
        info.episode = caps.get(2).and_then(|m| m.as_str().parse().ok());
        info.media_type = MediaType::Series;

        if let Some(m) = EPISODE_RE.find(&file_stem) {
            let raw_title = &file_stem[..m.start()];
            info.title = clean_title(raw_title);
        }
    } else {
        info.media_type = MediaType::Movie;
        info.title = clean_title(&file_stem);
    }

    if let Some(caps) = YEAR_RE.captures(&file_stem) {
        let year = caps.get(1).and_then(|m| m.as_str().parse().ok());
        info.year = year;

        match (&info.media_type, year) {
            (MediaType::Movie, _) => {
                if let Some(m) = YEAR_RE.find(&file_stem) {
                    info.title = clean_title(&file_stem[..m.start()]);
                }
            }
            (MediaType::Series, Some(y)) => {
                info.title = info.title.replace(&y.to_string(), "").trim().to_string();
            }
            _ => {}
        }
    }

    if info.media_type == MediaType::Series
        && info.title.is_empty()
        && let Some(parent) = path.parent()
        && let Some(dir_name) = parent.file_name().and_then(|s| s.to_str())
    {
        if !dir_name.to_lowercase().starts_with("season") {
            info.title = clean_title(dir_name);
        } else if let Some(grandparent) = parent.parent()
            && let Some(gp_name) = grandparent.file_name().and_then(|s| s.to_str())
        {
            info.title = clean_title(gp_name);
        }
    }

    info
}

fn clean_title(raw: &str) -> String {
    let cleaned = CLEAN_RE.replace_all(raw, " ");
    cleaned.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn try_parse_nfo(video_path: &Path) -> Option<MediaInfo> {
    let stem = video_path.file_stem()?.to_str()?;
    let parent = video_path.parent()?;

    let nfo_path = parent.join(format!("{stem}.nfo"));
    if !nfo_path.exists() {
        let movie_nfo = parent.join("movie.nfo");
        let tvshow_nfo = parent.join("tvshow.nfo");
        if movie_nfo.exists() {
            return parse_nfo_content(&movie_nfo, MediaType::Movie);
        }
        if tvshow_nfo.exists() {
            return parse_nfo_content(&tvshow_nfo, MediaType::Series);
        }

        return None;
    }

    parse_nfo_content(&nfo_path, MediaType::Movie)
}

fn parse_nfo_content(nfo_path: &Path, default_type: MediaType) -> Option<MediaInfo> {
    let content = read_to_string(nfo_path).ok()?;
    let mut info = MediaInfo {
        media_type: default_type,
        ..Default::default()
    };

    info.title = extract_xml_tag(&content, "title").unwrap_or_default();
    info.year = extract_xml_tag(&content, "year").and_then(|y| y.parse().ok());

    if content.contains("<episodedetails>") || content.contains("<tvshow>") {
        info.media_type = MediaType::Series;
    }

    if let Some(season) = extract_xml_tag(&content, "season") {
        info.season = season.parse().ok();
    }
    if let Some(episode) = extract_xml_tag(&content, "episode") {
        info.episode = episode.parse().ok();
    }

    if info.title.is_empty() {
        return None;
    }

    Some(info)
}

fn extract_xml_tag(content: &str, tag: &str) -> Option<String> {
    let open = format!("<{tag}>");
    let close = format!("</{tag}>");
    let start = content.find(&open)? + open.len();
    let end = content[start..].find(&close)? + start;
    let value = content[start..end].trim().to_string();

    if value.is_empty() { None } else { Some(value) }
}
