use crate::{
    commands::{Config, not_available},
    entities::{
        BillingPeriod, DateTime, Duration, Error, Money, Rate, Result, RoundMode, Tag, WorkPeriod,
        WorkTime, split_value_unit,
    },
    jobber::{Deleted, JobberGet, JobberIter},
};
use clap::ValueEnum;
use color_print::cformat;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use strum_macros::Display;

#[derive(Copy, Clone)]
pub(crate) enum Limit {
    None,
    Day,
    Week,
    Month,
}

#[derive(Debug, PartialEq, ValueEnum, Display, Copy, Clone, Serialize, Deserialize)]
#[clap(rename_all = "kebab-case")]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub(crate) enum Base {
    #[serde(alias = "m", alias = "min", alias = "minute")]
    Minutes,
    #[serde(alias = "h", alias = "hrs", alias = "hour")]
    Hours,
    #[serde(alias = "d", alias = "day")]
    Days,
    #[serde(alias = "w", alias = "week")]
    Weeks,
}

impl FromStr for Base {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "m" | "min" | "minute" | "minutes" => Ok(Base::Minutes),
            "h" | "hrs" | "hour" | "hours" => Ok(Base::Hours),
            "d" | "day" | "days" => Ok(Base::Days),
            "w" | "week" | "weeks" => Ok(Base::Weeks),
            other => Err(Error::UnknownInterval(other.to_string())),
        }
    }
}

#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub(crate) struct WorkingInterval {
    value: u32,
    base: Base,
}

impl std::fmt::Display for WorkingInterval {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}{}", self.value, self.base)
    }
}

impl Default for WorkingInterval {
    fn default() -> Self {
        Self {
            value: 15,
            base: Base::Minutes,
        }
    }
}

impl WorkingInterval {
    pub(crate) fn new(value: u32, base: Base) -> WorkingInterval {
        Self { value, base }
    }

    pub(crate) fn num_minutes(&self) -> u32 {
        match self.base {
            Base::Minutes => self.value,
            Base::Hours => self.value * 60,
            Base::Days => self.value * 60 * 24,
            Base::Weeks => self.value * 60 * 24 * 7,
        }
    }

    pub(crate) fn duration(&self) -> chrono::TimeDelta {
        Duration::minutes(self.num_minutes() as i64)
    }
}

impl FromStr for WorkingInterval {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let (value, unit) = split_value_unit(s)?;
        Ok(WorkingInterval::new(value, unit.parse()?))
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Scheme {
    pub(crate) rate: Option<Rate>,
    pub(crate) billing_period: Option<BillingPeriod>,
    pub(crate) min_work_interval: WorkingInterval,
}

fn merge_work_times(mut work_times: Vec<&WorkTime>) -> Vec<WorkTime> {
    work_times.sort_by_key(|wt| wt.start());
    work_times.into_iter().fold(Vec::new(), |mut merged, wt| {
        match merged.last_mut() {
            Some(last) if wt.start() <= last.end() => {
                last.set(last.start(), last.end().max(wt.end()))
            }
            _ => merged.push(wt.clone()),
        }
        merged
    })
}

impl Default for Scheme {
    fn default() -> Self {
        Self::new(None, None, Default::default()).expect("invalid default working scheme")
    }
}

impl Scheme {
    pub(crate) fn new(
        rate: Option<Rate>,
        billing: Option<BillingPeriod>,
        interval: WorkingInterval,
    ) -> Result<Scheme> {
        if 1440 % interval.num_minutes() != 0 {
            return Err(Error::InvalidWorkingInterval(interval.to_string()));
        }
        Ok(Scheme {
            rate,
            billing_period: billing,
            min_work_interval: interval,
        })
    }

    pub(crate) fn measure_work<'a>(
        &self,
        work_times: impl Iterator<Item = &'a WorkTime>,
    ) -> Duration {
        let packed = self.pack(work_times);
        Duration::minutes(packed.iter().map(|wt| (wt.duration()).num_minutes()).sum())
    }

    pub(crate) fn measure_revenue(&self, worked: Duration) -> Result<Money> {
        if let Some(rate) = &self.rate {
            let units = match rate.work_period {
                WorkPeriod::Minutes(min) => worked.num_minutes() as u32 / min,
                WorkPeriod::Hours(hours) => worked.num_hours() as u32 / hours,
                WorkPeriod::Days(days) => worked.num_days() as u32 / days,
                WorkPeriod::Weeks(weeks) => worked.num_weeks() as u32 / weeks,
            };
            Ok(rate.amount.mul(units)?)
        } else {
            Err(Error::NoRate)
        }
    }

    fn pack<'a>(&self, work_times: impl Iterator<Item = &'a WorkTime>) -> Vec<WorkTime> {
        merge_work_times(work_times.collect())
    }

    pub(crate) fn round(&self, date_time: &DateTime, mode: RoundMode) -> DateTime {
        date_time.round_to_interval(self.min_work_interval, mode)
    }

    fn limit(&self, limit: Limit) -> Duration {
        self.limit_ex(limit, 1.0)
    }

    fn max_limit(&self, limit: Limit) -> Duration {
        self.limit_ex(limit, 1.25)
    }

    fn limit_ex(&self, limit: Limit, factor: f64) -> Duration {
        match limit {
            Limit::None => Duration::MAX,
            Limit::Day => Duration::hours((8.0 * factor) as i64),
            Limit::Week => Duration::hours((40.0 * factor) as i64),
            Limit::Month => Duration::hours((160.0 * factor) as i64),
        }
    }

    pub(crate) fn format_duration(&self, duration: Duration, limit: Limit, ansi: bool) -> String {
        let minutes = duration.num_minutes();
        let output = if minutes < 60 {
            format!("{minutes} min")
        } else {
            let hours = minutes / 60;
            format!("{hours}:{:02} hrs", minutes - hours * 60)
        };
        if ansi && duration > self.max_limit(limit) {
            cformat!("<s><r!>{output}</>")
        } else if ansi && duration > self.limit(limit) {
            cformat!("<s><y!>{output}</>")
        } else if ansi && duration.is_zero() {
            cformat!("<dim>-</>")
        } else {
            output
        }
    }

    pub(crate) fn format_hours(&self, hours: f32, limit: Limit, ansi: bool) -> String {
        let output = if hours == 0.0 {
            not_available(ansi)
        } else {
            format!("{hours:.2}")
                .trim_end_matches('0')
                .trim_end_matches('.')
                .to_string()
        };
        let duration = Duration::minutes((hours * 60.0) as i64);
        if ansi && duration > self.max_limit(limit) {
            cformat!("<s><r!>{output}</>")
        } else if ansi && duration > self.limit(limit) {
            cformat!("<s><y!>{output}</>")
        } else if ansi && duration.is_zero() {
            cformat!("<dim>{output}</>")
        } else if ansi {
            cformat!("<s>{output}</>")
        } else {
            output
        }
    }

    pub(crate) fn format_date_time(&self, date_time: DateTime) -> String {
        date_time.format("%Y %a %b %-d %H:%M").to_string()
    }

    pub(crate) fn format_time(&self, date_time: DateTime) -> String {
        date_time.format("%H:%M").to_string()
    }

    pub(crate) fn format_revenue(&self, worked: Duration, ansi: bool) -> String {
        if let Some(rate) = &self.rate {
            let units = match rate.work_period {
                WorkPeriod::Minutes(min) => worked.num_minutes() as u32 / min,
                WorkPeriod::Hours(hours) => worked.num_hours() as u32 / hours,
                WorkPeriod::Days(days) => worked.num_days() as u32 / days,
                WorkPeriod::Weeks(weeks) => worked.num_weeks() as u32 / weeks,
            };
            format_revenue(rate.amount.mul(units).expect("money error"), ansi)
        } else if ansi {
            cformat!("<dim>n/a</>")
        } else {
            "n/a".to_string()
        }
    }
}

pub(crate) fn format_opt<T: ToString + Clone>(t: &Option<T>, ansi: bool) -> String {
    t.clone().map_or(not_available(ansi), |p| p.to_string())
}

pub(crate) fn format_duration(duration: Duration, limit: Limit, ansi: bool) -> String {
    Scheme::default().format_duration(duration, limit, ansi)
}

pub(crate) fn format_revenue(revenue: Money, ansi: bool) -> String {
    if ansi {
        if revenue.is_zero() {
            cformat!("<dim>-</>")
        } else {
            cformat!("<s><u>{revenue}</>",)
        }
    } else {
        revenue.to_string()
    }
}

pub(crate) fn format_hours(hours: f32, limit: Limit, ansi: bool) -> String {
    Scheme::default().format_hours(hours, limit, ansi)
}

pub(crate) fn format_tag(tag_id: &str, tag: &Tag, ansi: bool) -> String {
    let tag_id = if ansi {
        match *tag.color % 14 {
            0 => cformat!("<#000000><bg:#00FFFF> {tag_id} </>"),
            1 => cformat!("<#000000><bg:#FF00FF> {tag_id} </>"),
            2 => cformat!("<#000000><bg:#FFFF00> {tag_id} </>"),
            3 => cformat!("<#000000><bg:#0000FF> {tag_id} </>"),
            4 => cformat!("<#000000><bg:#00FF00> {tag_id} </>"),
            5 => cformat!("<#000000><bg:#FF0000> {tag_id} </>"),
            6 => cformat!("<#000000><bg:#FFFFFF> {tag_id} </>"),
            7 => cformat!("<#000000><bg:#007F7F> {tag_id} </>"),
            8 => cformat!("<#000000><bg:#7F007F> {tag_id} </>"),
            9 => cformat!("<#000000><bg:#7F7F00> {tag_id} </>"),
            10 => cformat!("<#000000><bg:#00007F> {tag_id} </>"),
            11 => cformat!("<#000000><bg:#007F00> {tag_id} </>"),
            12 => cformat!("<#000000><bg:#7F0000> {tag_id} </>"),
            13 => cformat!("<#000000><bg:#7F7F7F> {tag_id} </>"),
            _ => unreachable!(),
        }
    } else {
        tag_id.to_string()
    };

    if tag.deleted {
        if ansi {
            cformat!("❌ {tag_id}")
        } else {
            format!("(deleted) {tag_id}")
        }
    } else {
        tag_id
    }
}

pub(crate) fn format_date(date: DateTime) -> String {
    date.format("%x").to_string()
}

pub(crate) fn format_invoice_no(
    no: usize,
    deleted: bool,
    ansi: bool,
) -> crate::database::Result<String> {
    format_id(&no, false, false, deleted, ansi)
}

pub(crate) fn format_client_id(
    client_id: &str,
    database: &(impl JobberGet + JobberIter),
    config: &Config,
    ansi: bool,
) -> crate::database::Result<String> {
    let client = database.get_client(client_id)?;
    let deleted = client.deleted;
    let mut current = false;
    let running = database
        .iter_jobs(Deleted::No)
        .filter(|(_, job)| job.client == client_id)
        .inspect(|(job_id, _)| {
            if Some(*job_id) == config.current_job {
                current = true;
            }
        })
        .any(|(_, job)| job.running.is_some());
    format_id(&client_id, current, running, deleted, ansi)
}

pub(crate) fn format_job_id(
    job_id: usize,
    database: &impl JobberGet,
    config: &Config,
    ansi: bool,
) -> crate::database::Result<String> {
    let job = database.get_job(job_id)?;
    let deleted = job.deleted;
    let running = job.running.is_some();
    let current = Some(job_id) == config.current_job;
    format_id(&job_id, current, running, deleted, ansi)
}

pub(crate) fn format_worker_id(
    worker_id: &str,
    database: &(impl JobberGet + JobberIter),
    config: &Config,
    ansi: bool,
) -> crate::database::Result<String> {
    let worker = database.get_worker(worker_id)?;
    let deleted = worker.deleted;
    let mut current = false;
    let running = database
        .iter_jobs(Deleted::No)
        .filter(|(_, job)| job.worker == worker_id)
        .inspect(|(job_id, _)| {
            if Some(*job_id) == config.current_job {
                current = true;
            }
        })
        .any(|(_, job)| job.running.is_some());
    format_id(&worker_id, current, running, deleted, ansi)
}

fn format_id<T: std::fmt::Display>(
    id: &T,
    current: bool,
    running: bool,
    deleted: bool,
    ansi: bool,
) -> crate::database::Result<String> {
    if ansi {
        Ok(match (current, running, deleted) {
            (true, true, false) => cformat!("👷 <g!>▶</> {id}"),
            (true, false, false) => cformat!("<g!>▶</> {id}"),
            (false, true, false) => cformat!("👷 {id}"),
            (false, false, false) => id.to_string(),
            (.., true) => cformat!("❌ {id}"),
        })
    } else {
        Ok(match (current, running, deleted) {
            (true, true, false) => format!("(working) > {id}"),
            (true, false, false) => format!("(working) {id}"),
            (false, true, false) => format!("> {id}"),
            (false, false, false) => id.to_string(),
            (.., true) => format!("(deleted) {id}"),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::{from_end, from_start};

    fn wt(start: u32, end: u32) -> WorkTime {
        let start = from_start(&format!("2026-1-1 {start}:00")).unwrap();
        WorkTime::new(
            start,
            from_end(&format!("2026-1-1 {end}:00"), start).unwrap(),
        )
    }

    #[test]
    fn empty_input() {
        assert_eq!(merge_work_times(vec![]), vec![]);
    }

    #[test]
    fn single_entry() {
        assert_eq!(merge_work_times(vec![&wt(8, 10)]), vec![wt(8, 10)]);
    }

    #[test]
    fn non_overlapping() {
        let input = [wt(8, 10), wt(12, 14)];
        assert_eq!(
            merge_work_times(input.iter().collect()),
            vec![wt(8, 10), wt(12, 14)]
        );
    }

    #[test]
    fn overlapping() {
        let input = [wt(8, 12), wt(10, 14)];
        assert_eq!(merge_work_times(input.iter().collect()), vec![wt(8, 14)]);
    }

    #[test]
    fn adjacent() {
        let input = [wt(8, 10), wt(10, 12)];
        assert_eq!(merge_work_times(input.iter().collect()), vec![wt(8, 12)]);
    }

    #[test]
    fn contained() {
        let input = [wt(8, 16), wt(10, 12)];
        assert_eq!(merge_work_times(input.iter().collect()), vec![wt(8, 16)]);
    }

    #[test]
    fn unsorted_input() {
        let input = [wt(12, 14), wt(8, 10)];
        assert_eq!(
            merge_work_times(input.iter().collect()),
            vec![wt(8, 10), wt(12, 14)]
        );
    }

    #[test]
    fn multiple_merges() {
        let input = [wt(6, 9), wt(8, 11), wt(10, 14), wt(16, 18)];
        assert_eq!(
            merge_work_times(input.iter().collect()),
            vec![wt(6, 14), wt(16, 18)]
        );
    }
}
