/*
    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::{ImportPlugin, PluginContext, PluginInfo},
    source_rank::ObjectImportSourceExt,
};
use anyhow::{Context, Result, bail};
use jellycache::HashKey;
use jellycommon::*;
use jellydb::RowNum;
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",
            tag: MSOURCE_EXPLICIT,
            handle_file: true,
            ..Default::default()
        }
    }
    fn file(&self, ct: &PluginContext, row: RowNum, path: &Path) -> Result<()> {
        let filename = path.file_name().unwrap().to_string_lossy();
        let slot = match filename.as_ref() {
            "poster.jpeg" | "poster.webp" | "poster.png" => PICT_COVER,
            "backdrop.jpeg" | "backdrop.webp" | "backdrop.png" => PICT_BACKDROP,
            _ => return Ok(()),
        };
        let asset = ct.ic.cache.store(
            format!("media/literal/{}-{slot}.image", HashKey(path)),
            || {
                info!("import {slot} at {path:?}");
                let mut data = Vec::new();
                File::open(path)?.read_to_end(&mut data)?;
                Ok(data)
            },
        )?;
        ct.ic.db.transaction(&mut |txn| {
            let mut node = txn.get(row)?.unwrap();
            node = node.update(NO_PICTURES, |picts| picts.insert_s(ct.is, slot, &asset));
            txn.update(row, node)?;
            Ok(())
        })?;
        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",
            tag: MSOURCE_IMAGE_ATT,
            handle_media: true,
            ..Default::default()
        }
    }
    fn media(&self, ct: &PluginContext, row: RowNum, _path: &Path, seg: &Segment) -> Result<()> {
        let Some(cover) = seg
            .attachments
            .iter()
            .flat_map(|a| &a.files)
            .find(is_cover)
            .map(|att| String::from_utf8_lossy(&att.data))
        else {
            return Ok(());
        };

        ct.ic.update_node(row, |node| {
            node.update(NO_PICTURES, |picts| {
                picts.insert_s(ct.is, PICT_COVER, &cover)
            })
        })?;
        Ok(())
    }
}

pub struct General;
impl ImportPlugin for General {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "general",
            tag: MSOURCE_EXPLICIT,
            handle_instruction: true,
            ..Default::default()
        }
    }
    fn instruction(&self, ct: &PluginContext, node: RowNum, line: &str) -> Result<()> {
        if line == "hidden" {
            ct.ic.update_node(node, |node| {
                node.insert_s(ct.is, NO_VISIBILITY, VISI_HIDDEN)
            })?;
        }
        if line == "reduced" {
            ct.ic.update_node(node, |node| {
                node.insert_s(ct.is, NO_VISIBILITY, VISI_REDUCED)
            })?;
        }
        if let Some(kind) = line.strip_prefix("kind-").or(line.strip_prefix("kind=")) {
            let kind = match kind {
                "movie" => KIND_MOVIE,
                "video" => KIND_VIDEO,
                "music" => KIND_MUSIC,
                "short_form_video" => KIND_SHORTFORMVIDEO,
                "collection" => KIND_COLLECTION,
                "channel" => KIND_CHANNEL,
                "show" => KIND_SHOW,
                "series" => KIND_SERIES,
                "season" => KIND_SEASON,
                "episode" => KIND_EPISODE,
                _ => bail!("unknown node kind"),
            };
            ct.ic
                .update_node(node, |node| node.insert_s(ct.is, NO_KIND, kind))?;
        }
        if let Some(title) = line.strip_prefix("title=") {
            ct.ic
                .update_node(node, |node| node.insert_s(ct.is, NO_TITLE, title))?;
        }
        if let Some(index) = line.strip_prefix("index=") {
            let index = index.parse().context("parse index")?;
            ct.ic
                .update_node(node, |node| node.insert_s(ct.is, NO_INDEX, index))?;
        }
        Ok(())
    }
}

pub struct Children;
impl ImportPlugin for Children {
    fn info(&self) -> PluginInfo {
        PluginInfo {
            name: "children",
            tag: MSOURCE_EXPLICIT,
            handle_file: true,
            ..Default::default()
        }
    }
    fn file(&self, ct: &PluginContext, parent: RowNum, 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.ic
                    .update_node_slug(line, |n| n.extend(NO_PARENT, [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",
            tag: MSOURCE_IMAGE_ATT,
            handle_media: true,
            ..Default::default()
        }
    }
    fn media(&self, ct: &PluginContext, node: RowNum, 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::<u64>().context("parse episode num")?;
                let season = season
                    .unwrap_or("1")
                    .parse::<u64>()
                    .context("parse season num")?;

                ct.ic.update_node(node, |mut node| {
                    node = node.insert_s(ct.is, NO_SEASON_INDEX, season);
                    node = node.insert_s(ct.is, NO_INDEX, episode);
                    node = node.insert_s(ct.is, NO_KIND, KIND_EPISODE);
                    node
                })?;
            }
        }
        Ok(())
    }
}
