/*
    This file is part of jellything (https://codeberg.org/metamuffin/jellything)
    which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
    Copyright (C) 2026 metamuffin <metamuffin.org>
*/
use crate::plugins::{ImportContext, ImportPlugin, PluginInfo};
use anyhow::{Context, Result, bail};
use jellycache::{HashKey, cache_store};
use jellycommon::{Asset, NodeID, NodeKind, PictureSlot, Visibility};
use jellyremuxer::matroska::{AttachedFile, Segment};
use log::info;
use regex::Regex;
use std::{
    fs::{File, read_to_string},
    io::Read,
    path::Path,
    sync::LazyLock,
};

pub struct ImageFiles;
impl ImportPlugin for ImageFiles {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "image-files",
            handle_file: true,
            ..Default::default()
        }
    }
    fn file(&self, ct: &ImportContext, parent: NodeID, path: &Path) -> Result<()> {
        let filename = path.file_name().unwrap().to_string_lossy();
        let slot = match filename.as_ref() {
            "poster.jpeg" | "poster.webp" | "poster.png" => PictureSlot::Cover,
            "backdrop.jpeg" | "backdrop.webp" | "backdrop.png" => PictureSlot::Backdrop,
            _ => return Ok(()),
        };
        info!("import {slot} at {path:?}");
        let asset = Asset(cache_store(
            format!("media/literal/{}-{slot}.image", HashKey(path)),
            || {
                let mut data = Vec::new();
                File::open(path)?.read_to_end(&mut data)?;
                Ok(data)
            },
        )?);
        ct.db.update_node_init(parent, |node| {
            node.pictures.insert(slot, asset);
        })?;
        Ok(())
    }
}

pub fn is_cover(a: &&AttachedFile) -> bool {
    a.name.starts_with("cover") && a.media_type.starts_with("image/")
}
pub struct ImageAttachments;
impl ImportPlugin for ImageAttachments {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "image-attachments",
            handle_media: true,
            ..Default::default()
        }
    }
    fn media(&self, ct: &ImportContext, node: NodeID, _path: &Path, seg: &Segment) -> Result<()> {
        let Some(cover) = seg
            .attachments
            .iter()
            .flat_map(|a| &a.files)
            .find(is_cover)
            .map(|att| Asset(att.data.clone().try_into().unwrap()))
        else {
            return Ok(());
        };

        ct.db.update_node_init(node, |node| {
            node.pictures.insert(PictureSlot::Cover, cover);
        })?;
        Ok(())
    }
}

pub struct General;
impl ImportPlugin for General {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "general",
            handle_instruction: true,
            ..Default::default()
        }
    }
    fn instruction(&self, ct: &ImportContext, node: NodeID, line: &str) -> Result<()> {
        if line == "hidden" {
            ct.db.update_node_init(node, |node| {
                node.visibility = node.visibility.min(Visibility::Hidden);
            })?;
        }
        if line == "reduced" {
            ct.db.update_node_init(node, |node| {
                node.visibility = node.visibility.min(Visibility::Reduced);
            })?;
        }
        if let Some(kind) = line.strip_prefix("kind-").or(line.strip_prefix("kind=")) {
            let kind = match kind {
                "movie" => NodeKind::Movie,
                "video" => NodeKind::Video,
                "music" => NodeKind::Music,
                "short_form_video" => NodeKind::ShortFormVideo,
                "collection" => NodeKind::Collection,
                "channel" => NodeKind::Channel,
                "show" => NodeKind::Show,
                "series" => NodeKind::Series,
                "season" => NodeKind::Season,
                "episode" => NodeKind::Episode,
                _ => bail!("unknown node kind"),
            };
            ct.db.update_node_init(node, |node| {
                node.kind = kind;
            })?;
        }
        if let Some(title) = line.strip_prefix("title=") {
            ct.db.update_node_init(node, |node| {
                node.title = Some(title.to_owned());
            })?;
        }
        if let Some(index) = line.strip_prefix("index=") {
            let index = index.parse().context("parse index")?;
            ct.db.update_node_init(node, |node| {
                node.index = Some(index);
            })?;
        }
        Ok(())
    }
}

pub struct Children;
impl ImportPlugin for Children {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "children",
            handle_file: true,
            ..Default::default()
        }
    }
    fn file(&self, ct: &ImportContext, parent: NodeID, path: &Path) -> Result<()> {
        let filename = path.file_name().unwrap().to_string_lossy();
        if filename.as_ref() == "children" {
            info!("import children at {path:?}");
            for line in read_to_string(path)?.lines() {
                let line = line.trim();
                if line.starts_with("#") || line.is_empty() {
                    continue;
                }
                ct.db.update_node_init(NodeID::from_slug(line), |n| {
                    n.slug = line.to_owned();
                    n.parents.insert(parent);
                })?;
            }
        }
        Ok(())
    }
}

static RE_EPISODE_FILENAME: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"([sS](?<season>\d+))?([eE](?<episode>\d+))( (.+))?"#).unwrap());

pub struct EpisodeIndex;
impl ImportPlugin for EpisodeIndex {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "episode-info",
            handle_media: true,
            ..Default::default()
        }
    }
    fn media(&self, ct: &ImportContext, node: NodeID, path: &Path, _seg: &Segment) -> Result<()> {
        let filename = path.file_name().unwrap().to_string_lossy();
        if let Some(cap) = RE_EPISODE_FILENAME.captures(&filename) {
            if let Some(episode) = cap.name("episode").map(|m| m.as_str()) {
                let season = cap.name("season").map(|m| m.as_str());
                let episode = episode.parse::<usize>().context("parse episode num")?;
                let season = season
                    .unwrap_or("1")
                    .parse::<usize>()
                    .context("parse season num")?;

                ct.db.update_node_init(node, |node| {
                    node.kind = NodeKind::Episode;
                    node.index = Some(episode);
                    node.season_index = Some(season);
                })?;
            }
        }
        Ok(())
    }
}
