/*
    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::{DATABASE, filter_sort::filter_and_sort_nodes, session::Session};
use anyhow::{Result, anyhow};
use jellycommon::{
    IdentifierType, Node, NodeID, NodeKind, Visibility,
    api::{ApiNodeResponse, NodeFilterSort, SortOrder, SortProperty},
    user::{NodeUserData, WatchedState},
};
use jellydb::Database;
use std::{cmp::Reverse, collections::BTreeMap, sync::Arc};

pub fn get_node(
    session: &Session,
    id: NodeID,
    children: bool,
    parents: bool,
    filter: NodeFilterSort,
) -> Result<ApiNodeResponse> {
    let (node, udata) = DATABASE.get_node_with_userdata(id, session)?;

    let mut children = if children {
        DATABASE
            .get_node_children(id)?
            .into_iter()
            .map(|c| DATABASE.get_node_with_userdata(c, session))
            .collect::<anyhow::Result<Vec<_>>>()?
    } else {
        Vec::new()
    };

    let mut parents = if parents {
        node.parents
            .iter()
            .map(|pid| DATABASE.get_node_with_userdata(*pid, session))
            .collect::<anyhow::Result<Vec<_>>>()?
    } else {
        Vec::new()
    };

    let mut similar = get_similar_media(session, &node)?;

    similar.retain(|(n, _)| n.visibility >= Visibility::Reduced);
    children.retain(|(n, _)| n.visibility >= Visibility::Reduced);
    parents.retain(|(n, _)| n.visibility >= Visibility::Reduced);

    filter_and_sort_nodes(
        &filter,
        match node.kind {
            NodeKind::Channel => (SortProperty::ReleaseDate, SortOrder::Descending),
            NodeKind::Season | NodeKind::Show => (SortProperty::Index, SortOrder::Ascending),
            _ => (SortProperty::Title, SortOrder::Ascending),
        },
        &mut children,
    );

    Ok(ApiNodeResponse {
        children,
        parents,
        node,
        userdata: udata,
    })
}

pub fn get_similar_media(session: &Session, node: &Node) -> Result<Vec<(Arc<Node>, NodeUserData)>> {
    let this_id = NodeID::from_slug(&node.slug);
    let mut ranking = BTreeMap::<NodeID, usize>::new();
    for tag in &node.tags {
        let nodes = DATABASE.get_tag_nodes(tag)?;
        let weight = 1_000_000 / nodes.len();
        for n in nodes {
            if n != this_id {
                *ranking.entry(n).or_default() += weight;
            }
        }
    }
    let mut ranking = ranking.into_iter().collect::<Vec<_>>();
    ranking.sort_by_key(|(_, k)| Reverse(*k));
    ranking
        .into_iter()
        .take(32)
        .map(|(pid, _)| DATABASE.get_node_with_userdata(pid, session))
        .collect::<anyhow::Result<Vec<_>>>()
}

pub trait DatabaseNodeUserDataExt {
    fn get_node_with_userdata(
        &self,
        id: NodeID,
        session: &Session,
    ) -> Result<(Arc<Node>, NodeUserData)>;
}
impl DatabaseNodeUserDataExt for Database {
    fn get_node_with_userdata(
        &self,
        id: NodeID,
        session: &Session,
    ) -> Result<(Arc<Node>, NodeUserData)> {
        Ok((
            self.get_node(id)?.ok_or(anyhow!("node does not exist"))?,
            self.get_node_udata(id, &session.user.name)?
                .unwrap_or_default(),
        ))
    }
}

pub fn get_nodes_modified_since(_session: &Session, since: u64) -> Result<Vec<NodeID>> {
    let mut nodes = DATABASE.get_nodes_modified_since(since)?;
    nodes.retain(|id| {
        DATABASE.get_node(*id).is_ok_and(|n| {
            n.as_ref()
                .is_some_and(|n| n.visibility >= Visibility::Reduced)
        })
    });
    Ok(nodes)
}

pub fn get_node_by_eid(
    _session: &Session,
    ty: IdentifierType,
    value: &str,
) -> Result<Option<NodeID>> {
    DATABASE.get_node_by_identifier(ty, value)
}
pub fn node_id_to_slug(_session: &Session, id: NodeID) -> Result<String> {
    Ok(DATABASE
        .get_node(id)?
        .ok_or(anyhow!("node does not exist"))?
        .slug
        .to_owned())
}

pub fn update_node_userdata_watched(
    session: &Session,
    node: NodeID,
    state: WatchedState,
) -> Result<()> {
    // TODO perm
    DATABASE.update_node_udata(node, &session.user.name, |udata| {
        udata.watched = state;
        Ok(())
    })
}
pub fn update_node_userdata_watched_progress(
    session: &Session,
    node: NodeID,
    time: f64,
) -> Result<()> {
    // TODO perm
    DATABASE.update_node_udata(node, &session.user.name, |udata| {
        udata.watched = match udata.watched {
            WatchedState::None | WatchedState::Pending | WatchedState::Progress(_) => {
                WatchedState::Progress(time)
            }
            WatchedState::Watched => WatchedState::Watched,
        };
        Ok(())
    })
}
pub fn update_node_userdata_rating(session: &Session, node: NodeID, rating: i32) -> Result<()> {
    // TODO perm
    DATABASE.update_node_udata(node, &session.user.name, |udata| {
        udata.rating = rating;
        Ok(())
    })
}
