use std::{
    env,
    fmt::{self, Display},
    io::{StdoutLock, Write},
    time::Duration,
};

use atuin_common::utils;
use clap::Subcommand;
use eyre::Result;
use runtime_format::{FormatKey, FormatKeyError, ParsedFmt};

use atuin_client::{
    database::{current_context, Database},
    history::History,
    settings::Settings,
};

#[cfg(feature = "sync")]
use atuin_client::sync;
use log::debug;

use super::search::format_duration;
use super::search::format_duration_into;

#[derive(Subcommand)]
#[command(infer_subcommands = true)]
pub enum Cmd {
    /// Begins a new command in the history
    Start { command: Vec<String> },

    /// Finishes a new command in the history (adds time, exit code)
    End {
        id: String,
        #[arg(long, short)]
        exit: i64,
    },

    /// List all items in history
    List {
        #[arg(long, short)]
        cwd: bool,

        #[arg(long, short)]
        session: bool,

        #[arg(long)]
        human: bool,

        /// Show only the text of the command
        #[arg(long)]
        cmd_only: bool,

        /// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
        /// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
        #[arg(long, short)]
        format: Option<String>,
    },

    /// Get the last command ran
    Last {
        #[arg(long)]
        human: bool,

        /// Show only the text of the command
        #[arg(long)]
        cmd_only: bool,

        /// Available variables: {command}, {directory}, {duration}, {user}, {host} and {time}.
        /// Example: --format "{time} - [{duration}] - {directory}$\t{command}"
        #[arg(long, short)]
        format: Option<String>,
    },
}

#[derive(Clone, Copy, Debug)]
pub enum ListMode {
    Human,
    CmdOnly,
    Regular,
}

impl ListMode {
    pub const fn from_flags(human: bool, cmd_only: bool) -> Self {
        if human {
            ListMode::Human
        } else if cmd_only {
            ListMode::CmdOnly
        } else {
            ListMode::Regular
        }
    }
}

#[allow(clippy::cast_sign_loss)]
pub fn print_list(h: &[History], list_mode: ListMode, format: Option<&str>) {
    let w = std::io::stdout();
    let mut w = w.lock();

    match list_mode {
        ListMode::Human => print_human_list(&mut w, h, format),
        ListMode::CmdOnly => print_cmd_only(&mut w, h),
        ListMode::Regular => print_regular(&mut w, h, format),
    }

    w.flush().expect("failed to flush history");
}

/// type wrapper around `History` so we can implement traits
struct FmtHistory<'a>(&'a History);

/// defines how to format the history
impl FormatKey for FmtHistory<'_> {
    #[allow(clippy::cast_sign_loss)]
    fn fmt(&self, key: &str, f: &mut fmt::Formatter<'_>) -> Result<(), FormatKeyError> {
        match key {
            "command" => f.write_str(self.0.command.trim())?,
            "directory" => f.write_str(self.0.cwd.trim())?,
            "exit" => f.write_str(&self.0.exit.to_string())?,
            "duration" => {
                let dur = Duration::from_nanos(std::cmp::max(self.0.duration, 0) as u64);
                format_duration_into(dur, f)?;
            }
            "time" => self.0.timestamp.format("%Y-%m-%d %H:%M:%S").fmt(f)?,
            "relativetime" => {
                let since = chrono::Utc::now() - self.0.timestamp;
                let time = format_duration(since.to_std().unwrap_or_default());
                f.write_str(&time)?;
            }
            "host" => f.write_str(
                self.0
                    .hostname
                    .split_once(':')
                    .map_or(&self.0.hostname, |(host, _)| host),
            )?,
            "user" => f.write_str(self.0.hostname.split_once(':').map_or("", |(_, user)| user))?,
            _ => return Err(FormatKeyError::UnknownKey),
        }
        Ok(())
    }
}

fn print_list_with(w: &mut StdoutLock, h: &[History], format: &str) {
    let fmt = match ParsedFmt::new(format) {
        Ok(fmt) => fmt,
        Err(err) => {
            eprintln!("ERROR: History formatting failed with the following error: {err}");
            println!("If your formatting string contains curly braces (eg: {{var}}) you need to escape them this way: {{{{var}}.");
            std::process::exit(1)
        }
    };

    for h in h.iter().rev() {
        writeln!(w, "{}", fmt.with_args(&FmtHistory(h))).expect("failed to write history");
    }
}

pub fn print_human_list(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
    let format = format
        .unwrap_or("{time} · {duration}\t{command}")
        .replace("\\t", "\t");
    print_list_with(w, h, &format);
}

pub fn print_regular(w: &mut StdoutLock, h: &[History], format: Option<&str>) {
    let format = format
        .unwrap_or("{time}\t{command}\t{duration}")
        .replace("\\t", "\t");
    print_list_with(w, h, &format);
}

pub fn print_cmd_only(w: &mut StdoutLock, h: &[History]) {
    for h in h.iter().rev() {
        writeln!(w, "{}", h.command.trim()).expect("failed to write history");
    }
}

impl Cmd {
    pub async fn run(&self, settings: &Settings, db: &mut impl Database) -> Result<()> {
        let context = current_context();

        match self {
            Self::Start { command: words } => {
                let command = words.join(" ");

                if command.starts_with(' ') || settings.history_filter.is_match(&command) {
                    return Ok(());
                }

                // It's better for atuin to silently fail here and attempt to
                // store whatever is ran, than to throw an error to the terminal
                let cwd = utils::get_current_dir();

                let h = History::new(chrono::Utc::now(), command, cwd, -1, -1, None, None, None);

                // print the ID
                // we use this as the key for calling end
                println!("{}", h.id);
                db.save(&h).await?;
                Ok(())
            }

            Self::End { id, exit } => {
                if id.trim() == "" {
                    return Ok(());
                }

                let mut h = db.load(id).await?;

                if h.duration > 0 {
                    debug!("cannot end history - already has duration");

                    // returning OK as this can occur if someone Ctrl-c a prompt
                    return Ok(());
                }

                h.exit = *exit;
                h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos();

                db.update(&h).await?;

                if settings.should_sync()? {
                    #[cfg(feature = "sync")]
                    {
                        debug!("running periodic background sync");
                        sync::sync(settings, false, db).await?;
                    }
                    #[cfg(not(feature = "sync"))]
                    debug!("not compiled with sync support");
                } else {
                    debug!("sync disabled! not syncing");
                }

                Ok(())
            }

            Self::List {
                session,
                cwd,
                human,
                cmd_only,
                format,
            } => {
                let session = if *session {
                    Some(env::var("ATUIN_SESSION")?)
                } else {
                    None
                };
                let cwd = if *cwd {
                    Some(utils::get_current_dir())
                } else {
                    None
                };

                let history = match (session, cwd) {
                    (None, None) => db.list(settings.filter_mode, &context, None, false).await?,
                    (None, Some(cwd)) => {
                        let query = format!("select * from history where cwd = '{cwd}';");
                        db.query_history(&query).await?
                    }
                    (Some(session), None) => {
                        let query = format!("select * from history where session = '{session}';");
                        db.query_history(&query).await?
                    }
                    (Some(session), Some(cwd)) => {
                        let query = format!(
                            "select * from history where cwd = '{cwd}' and session = '{session}';",
                        );
                        db.query_history(&query).await?
                    }
                };

                print_list(
                    &history,
                    ListMode::from_flags(*human, *cmd_only),
                    format.as_deref(),
                );

                Ok(())
            }

            Self::Last {
                human,
                cmd_only,
                format,
            } => {
                let last = db.last().await?;
                print_list(
                    &[last],
                    ListMode::from_flags(*human, *cmd_only),
                    format.as_deref(),
                );

                Ok(())
            }
        }
    }
}
