use std::{
    cmp::Ordering,
    collections::{
        hash_map::Entry::{Occupied, Vacant},
        BinaryHeap,
        HashMap,
        HashSet,
    },
    time::{SystemTime, UNIX_EPOCH},
};

use anyhow::{anyhow, Error};
use cln_rpc::{
    model::requests::SendpayRoute,
    primitives::{Amount, ShortChannelId, ShortChannelIdDir},
};
use sling::{Job, SatDirection};

use crate::{
    model::{Config, DijkstraNode, JobMessage, Liquidity, LnGraph, PubKeyBytes, Task},
    util::{edge_cost, fee_total_msat_precise},
};
pub fn dijkstra(
    config: &Config,
    lngraph: &LnGraph,
    job: &Job,
    task: &mut Task,
    actual_candidates: &[ShortChannelId],
    excepts: &[ShortChannelIdDir],
    liquidity: &HashMap<ShortChannelIdDir, Liquidity>,
) -> Result<Vec<SendpayRoute>, Error> {
    let two_weeks_ago = u32::try_from(
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
            - 60 * 60 * 24 * 14,
    )?;

    let mut visited = HashSet::with_capacity(lngraph.node_count());
    let mut scores = HashMap::new();
    let mut predecessor = HashMap::new();
    let mut visit_next = BinaryHeap::new();
    let zero_score = u64::default();

    let (start, goal) = match job.sat_direction {
        SatDirection::Pull => (config.pubkey_bytes, task.other_pubkey),
        SatDirection::Push => (task.other_pubkey, config.pubkey_bytes),
    };
    let slingchan = construct_first_node(task, config, lngraph, job.sat_direction, start)?;
    scores.insert(&start, slingchan);
    visit_next.push(MinScored(zero_score, &start));
    while let Some(MinScored(node_score, node)) = visit_next.pop() {
        if visited.contains(&node) {
            // debug!(
            //     "{}: already visited: {}",
            //     slingchan.channel.short_channel_id.to_string(),
            //     &node
            // );
            continue;
        }
        if &goal == node {
            // debug!(
            //     "{}: arrived at goal: {}  {}",
            //     slingchan.channel.short_channel_id.to_string(),
            //     &goal,
            //     &node
            // );
            break;
        }
        let current_hops = scores.get(&node).unwrap().hops;
        if current_hops + 2 > job.get_maxhops(config.maxhops) {
            continue;
        }
        for (scid, edge) in lngraph.edges(
            node,
            two_weeks_ago,
            actual_candidates,
            config,
            job,
            excepts,
            liquidity,
        ) {
            if visited.contains(&edge.destination) {
                // debug!(
                //     "{}: already visited: {}",
                //     slingchan.channel.short_channel_id.to_string(),
                //     &next
                // );
                continue;
            }
            let next_score = if edge.source == config.pubkey_bytes {
                0
            } else {
                node_score + edge_cost(edge, job.amount_msat)
            };
            // debug!(
            //     "{}: next: {} node_score:{} next_score:{}",
            //     slingchan.channel.short_channel_id.to_string(),
            //     &next,
            //     &node_score,
            //     &next_score
            // );

            match scores.entry(&edge.destination) {
                Occupied(ent) => {
                    if next_score < ent.get().score {
                        // debug!(
                        //     "{}: found better path to: {}",
                        //     slingchan.channel.short_channel_id.to_string(),
                        //     &next
                        // );
                        let dijkstra_node = DijkstraNode {
                            score: next_score,
                            channel_state: *edge,
                            destination: edge.destination,
                            hops: current_hops + 1,
                            short_channel_id: scid.short_channel_id,
                        };
                        *ent.into_mut() = dijkstra_node;
                        visit_next.push(MinScored(next_score, &edge.destination));
                        predecessor.insert(&edge.destination, node);
                    }
                }
                Vacant(ent) => {
                    // debug!(
                    //     "{}: found new path to: {} via {}",
                    //     slingchan.channel.short_channel_id.to_string(),
                    //     &next,
                    //     &edge.channel.short_channel_id.to_string()
                    // );
                    let dijkstra_node = DijkstraNode {
                        score: next_score,
                        channel_state: *edge,
                        destination: edge.destination,
                        hops: current_hops + 1,
                        short_channel_id: scid.short_channel_id,
                    };
                    ent.insert(dijkstra_node);
                    visit_next.push(MinScored(next_score, &edge.destination));
                    predecessor.insert(&edge.destination, node);
                }
            }
        }
        visited.insert(node);
    }

    Ok(build_route(
        &predecessor,
        &goal,
        &scores,
        job,
        &start,
        &slingchan,
        config,
    ))
}

fn build_route(
    predecessor: &HashMap<&PubKeyBytes, &PubKeyBytes>,
    goal: &PubKeyBytes,
    scores: &HashMap<&PubKeyBytes, DijkstraNode>,
    job: &Job,
    start: &PubKeyBytes,
    slingchan: &DijkstraNode,
    config: &Config,
) -> Vec<SendpayRoute> {
    let mut dijkstra_path = Vec::new();
    // debug!("predecssors: {:?}", predecessor);
    let mut prev;
    match predecessor.get(goal) {
        Some(node) => prev = *node,
        None => return vec![],
    }
    dijkstra_path.push(*scores.get(goal).unwrap());
    // debug!(
    //     "{}: found potential new route with #hops: {}",
    //     slingchan.channel.short_channel_id.to_string(),
    //     dijkstra_path.get(0).unwrap().hops
    // );

    while prev != start {
        let spr = scores.get(prev).unwrap();

        prev = predecessor.get(prev).unwrap();
        dijkstra_path.push(*spr);
    }
    match job.sat_direction {
        SatDirection::Pull => dijkstra_path.insert(0, *slingchan),
        SatDirection::Push => dijkstra_path.push(*slingchan),
    }

    let mut sendpay_route = Vec::new();
    let mut prev_amount_msat;
    let mut amount_msat = Amount::from_msat(0);
    let mut delay = config.cltv_delta;

    let first_hop = if dijkstra_path.is_empty() {
        return sendpay_route;
    } else {
        dijkstra_path.first().unwrap()
    };

    for hop in &dijkstra_path {
        if hop == first_hop {
            let routing_scid = if let Some(rscid) = first_hop.channel_state.scid_alias {
                rscid
            } else {
                first_hop.short_channel_id
            };
            sendpay_route.insert(
                0,
                SendpayRoute {
                    amount_msat: Amount::from_msat(job.amount_msat),
                    id: dijkstra_path.first().unwrap().destination.to_pubkey(),
                    delay,
                    channel: routing_scid,
                },
            );
        } else {
            let routing_scid = if let Some(rscid) = hop.channel_state.scid_alias {
                rscid
            } else {
                hop.short_channel_id
            };
            sendpay_route.insert(
                0,
                SendpayRoute {
                    amount_msat,
                    id: hop.destination.to_pubkey(),
                    delay,
                    channel: routing_scid,
                },
            );
        }
        prev_amount_msat = sendpay_route.first().unwrap().amount_msat;
        amount_msat = Amount::from_msat(
            Amount::msat(&prev_amount_msat)
                + fee_total_msat_precise(
                    hop.channel_state.fee_per_millionth,
                    hop.channel_state.base_fee_millisatoshi,
                    Amount::msat(&prev_amount_msat),
                )
                .ceil() as u64,
        );
        delay += hop.channel_state.delay;
    }
    sendpay_route
}

fn construct_first_node(
    task: &mut Task,
    config: &Config,
    lngraph: &LnGraph,
    sat_direction: SatDirection,
    destination: PubKeyBytes,
) -> Result<DijkstraNode, Error> {
    match sat_direction {
        SatDirection::Pull => {
            let Ok((_scid_dir, slingchan_inc)) =
                lngraph.get_state_no_direction(&task.other_pubkey, task.get_chan_id())
            else {
                log::warn!("{task}: channel not found in graph!");
                task.set_state(JobMessage::ChanNotInGraph);
                return Err(anyhow!("channel not found in graph"));
            };
            Ok(DijkstraNode {
                score: 0,
                channel_state: *slingchan_inc,
                destination,
                short_channel_id: task.get_chan_id(),
                hops: 0,
            })
        }
        SatDirection::Push => {
            let Ok((_scid_dir, slingchan_out)) =
                lngraph.get_state_no_direction(&config.pubkey_bytes, task.get_chan_id())
            else {
                log::warn!("{task}: channel not found in graph!");
                task.set_state(JobMessage::ChanNotInGraph);
                return Err(anyhow!("channel not found in graph"));
            };
            Ok(DijkstraNode {
                score: 0,
                channel_state: *slingchan_out,
                destination,
                short_channel_id: task.get_chan_id(),
                hops: 0,
            })
        }
    }
}

/// `MinScored<K, T>` holds a score `K` and a scored object `T` in
/// a pair for use with a `BinaryHeap`.
///
/// `MinScored` compares in reverse order by the score, so that we can
/// use `BinaryHeap` as a min-heap to extract the score-value pair with the
/// least score.
///
/// **Note:** `MinScored` implements a total order (`Ord`), so that it is
/// possible to use float types as scores.
#[derive(Copy, Clone, Debug)]
pub struct MinScored<K, T>(pub K, pub T);

impl<K: PartialOrd, T> PartialEq for MinScored<K, T> {
    #[inline]
    fn eq(&self, other: &MinScored<K, T>) -> bool {
        self.cmp(other) == Ordering::Equal
    }
}

impl<K: PartialOrd, T> Eq for MinScored<K, T> {}

impl<K: PartialOrd, T> PartialOrd for MinScored<K, T> {
    #[inline]
    fn partial_cmp(&self, other: &MinScored<K, T>) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl<K: PartialOrd, T> Ord for MinScored<K, T> {
    #[inline]
    fn cmp(&self, other: &MinScored<K, T>) -> Ordering {
        let a = &self.0;
        let b = &other.0;
        if a == b {
            Ordering::Equal
        } else if a < b {
            Ordering::Greater
        } else if a > b {
            Ordering::Less
        } else if a.ne(a) && b.ne(b) {
            // these are the NaN cases
            Ordering::Equal
        } else if a.ne(a) {
            // Order NaN less, so that it is last in the MinScore order
            Ordering::Less
        } else {
            Ordering::Greater
        }
    }
}
