#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]

use flate2::write::GzEncoder;
use rustyline::completion::Completer;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::FileHistory;
use rustyline::validate::Validator;
use rustyline::{Editor, Helper};
use std::borrow::Cow;
use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::PathBuf;
use std::sync::Arc;
use time::{Duration, OffsetDateTime};
use tracing::Subscriber;
use tracing_subscriber::Layer;
use tracing_subscriber::filter::LevelFilter;

use crate::command::CommandSender;
use crate::command::string_reader::StringReader;
use crate::command::tree::NodeType;
use crate::server::Server;

#[macro_export]
macro_rules! log_at_level {
    ($level:expr, $($arg:tt)*) => {
        match $level {
            tracing::Level::TRACE => tracing::trace!($($arg)*),
            tracing::Level::DEBUG => tracing::debug!($($arg)*),
            tracing::Level::INFO => tracing::info!($($arg)*),
            tracing::Level::WARN => tracing::warn!($($arg)*),
            tracing::Level::ERROR => tracing::error!($($arg)*),
        }
    };
}

#[macro_export]
macro_rules! plugin_log {
    ($level:expr, $plugin_name:expr, $($arg:tt)*) => {{
        let plugin_name = $plugin_name;
        match $level {
            tracing::Level::TRACE => {
                tracing::trace!(
                    target: "pumpkin_plugin",
                    plugin = plugin_name,
                    $($arg)*
                )
            },
            tracing::Level::DEBUG => {
                tracing::debug!(
                    target: "pumpkin_plugin",
                    plugin = plugin_name,
                    $($arg)*
                )
            },
            tracing::Level::INFO => {
                tracing::info!(
                    target: "pumpkin_plugin",
                    plugin = plugin_name,
                    $($arg)*
                )
            },
            tracing::Level::WARN => {
                tracing::warn!(
                    target: "pumpkin_plugin",
                    plugin = plugin_name,
                    $($arg)*
                )
            },
            tracing::Level::ERROR => {
                tracing::error!(
                    target: "pumpkin_plugin",
                    plugin = plugin_name,
                    $($arg)*
                )
            },
        }
    }};
}

const LOG_DIR: &str = "logs";
const MAX_ATTEMPTS: u32 = 1000;

/// A wrapper for our logger to hold the terminal input while no input is expected in order to
/// properly flush logs to the output while they happen instead of batched
pub struct ReadlineLogWrapper {
    readline: std::sync::Mutex<Option<Editor<PumpkinCommandCompleter, FileHistory>>>,
}

struct GzipRollingLoggerData {
    pub current_day_of_month: u8,
    pub last_rotate_time: time::OffsetDateTime,
    pub file: BufWriter<File>,
    latest_filename: String,
}

pub struct GzipRollingLogger {
    log_level: LevelFilter,
    data: std::sync::Mutex<GzipRollingLoggerData>,
}

impl GzipRollingLogger {
    pub fn new(
        log_level: LevelFilter,
        filename: String,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let now = time::OffsetDateTime::now_utc();
        std::fs::create_dir_all(LOG_DIR)?;

        let latest_path = PathBuf::from(LOG_DIR).join(&filename);

        // If latest.log exists, we will gzip it
        if latest_path.exists() {
            eprintln!(
                "Found existing log file at '{}', gzipping it now...",
                latest_path.display()
            );

            let new_gz_path = Self::new_filename(true)?;

            let mut file = File::open(&latest_path)?;

            let mut encoder = GzEncoder::new(
                BufWriter::new(File::create(&new_gz_path)?),
                flate2::Compression::best(),
            );

            io::copy(&mut file, &mut encoder)?;
            encoder.finish()?;

            std::fs::remove_file(&latest_path)?;
        }

        let file = BufWriter::new(File::create(&latest_path)?);

        Ok(Self {
            log_level,
            data: std::sync::Mutex::new(GzipRollingLoggerData {
                current_day_of_month: now.day(),
                last_rotate_time: now,
                latest_filename: filename,
                file,
            }),
        })
    }

    pub fn new_filename(yesterday: bool) -> Result<PathBuf, Box<dyn std::error::Error>> {
        let mut now = OffsetDateTime::now_utc();

        if yesterday {
            now -= Duration::days(1);
        }

        let date_format = format!("{}-{:02}-{:02}", now.year(), now.month() as u8, now.day());

        let log_path = PathBuf::from(LOG_DIR);

        let mut oldest_log = None;

        for id in 1..=MAX_ATTEMPTS {
            let filename = log_path.join(format!("{date_format}-{id}.log.gz"));

            if !filename.exists() {
                return Ok(filename);
            }

            let Ok(modified_time) = filename.metadata().and_then(|m| m.modified()) else {
                continue;
            };

            if let Some((_, old_time)) = oldest_log {
                if modified_time < old_time {
                    oldest_log = Some((filename, modified_time));
                }

                continue;
            }

            oldest_log = Some((filename, modified_time));
        }

        if let Some((path, _)) = oldest_log {
            eprintln!(
                "Max log ids ({MAX_ATTEMPTS}) used for {date_format}; overwriting oldest log file: {}",
                path.display()
            );
            return Ok(path);
        }

        Err(format!(
            "Failed to find a unique log filename for date {date_format} after {MAX_ATTEMPTS} attempts.",
        )
        .into())
    }

    fn rotate_log(&self) -> Result<(), Box<dyn std::error::Error>> {
        let now = time::OffsetDateTime::now_utc();
        let mut data = self
            .data
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);

        let new_gz_path = Self::new_filename(true)?;
        let latest_path = PathBuf::from(LOG_DIR).join(&data.latest_filename);

        // Flush and drop the current file
        data.file.flush()?;
        drop(std::mem::replace(
            &mut data.file,
            BufWriter::new(File::create("/dev/null")?),
        ));

        let mut file = File::open(&latest_path)?;
        let mut encoder = GzEncoder::new(
            BufWriter::new(File::create(&new_gz_path)?),
            flate2::Compression::best(),
        );
        io::copy(&mut file, &mut encoder)?;
        encoder.finish()?;

        data.current_day_of_month = now.day();
        data.last_rotate_time = now;
        data.file = BufWriter::new(File::create(&latest_path)?);
        Ok(())
    }
}

fn remove_ansi_color_code(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut it = s.chars();

    while let Some(c) = it.next() {
        if c == '\x1b' {
            for c_seq in it.by_ref() {
                if c_seq.is_ascii_alphabetic() {
                    break;
                }
            }
        } else {
            result.push(c);
        }
    }
    result
}

impl<S> Layer<S> for GzipRollingLogger
where
    S: Subscriber,
{
    fn on_event(
        &self,
        event: &tracing::Event<'_>,
        _ctx: tracing_subscriber::layer::Context<'_, S>,
    ) {
        let metadata = event.metadata();
        let level = metadata.level();

        // Check if we should log this event based on level
        let should_log = match *level {
            tracing::Level::ERROR => self.log_level >= LevelFilter::ERROR,
            tracing::Level::WARN => self.log_level >= LevelFilter::WARN,
            tracing::Level::INFO => self.log_level >= LevelFilter::INFO,
            tracing::Level::DEBUG => self.log_level >= LevelFilter::DEBUG,
            tracing::Level::TRACE => self.log_level >= LevelFilter::TRACE,
        };

        if !should_log {
            return;
        }

        let now = time::OffsetDateTime::now_utc();

        if let Ok(mut data) = self.data.lock() {
            // Format the event
            let mut visitor = StringVisitor::default();
            event.record(&mut visitor);
            let message = visitor.0;

            let clean_message = remove_ansi_color_code(&message);

            // Write to file
            let _ = writeln!(data.file, "[{level}] {clean_message}");
            let _ = data.file.flush();

            // Check if we need to rotate
            if data.current_day_of_month != now.day() {
                drop(data);
                if let Err(e) = self.rotate_log() {
                    eprintln!("Failed to rotate log: {e}");
                }
            }
        }
    }
}

#[derive(Default)]
struct StringVisitor(String);

impl tracing::field::Visit for StringVisitor {
    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
        if field.name() == "message" {
            self.0 = format!("{value:?}");
            // Remove quotes if present
            if self.0.starts_with('"') && self.0.ends_with('"') {
                self.0 = self.0[1..self.0.len() - 1].to_string();
            }
        }
    }
}

impl ReadlineLogWrapper {
    #[must_use]
    pub const fn new(rl: Option<Editor<PumpkinCommandCompleter, FileHistory>>) -> Self {
        Self {
            readline: std::sync::Mutex::new(rl),
        }
    }

    pub fn take_readline(&self) -> Option<Editor<PumpkinCommandCompleter, FileHistory>> {
        self.readline
            .lock()
            .map_or_else(|_| None, |mut result| result.take())
    }

    // This isn't really dead code. It is just only used by the lib and not the bin for this
    // crate, and as such creates a compiler warning.
    #[allow(dead_code)]
    pub fn return_readline(&self, rl: Editor<PumpkinCommandCompleter, FileHistory>) {
        if let Ok(mut result) = self.readline.lock() {
            let _ = result.insert(rl);
        }
    }
}

#[derive(Clone, Default)]
pub struct PumpkinCommandCompleter {
    pub server: Arc<std::sync::RwLock<Option<Arc<Server>>>>,
    pub rt: Arc<std::sync::OnceLock<tokio::runtime::Handle>>,
}

impl PumpkinCommandCompleter {
    #[must_use]
    pub fn new() -> Self {
        Self {
            server: Arc::new(std::sync::RwLock::new(None)),
            rt: Arc::new(std::sync::OnceLock::new()),
        }
    }
}

impl Helper for PumpkinCommandCompleter {}
impl Highlighter for PumpkinCommandCompleter {
    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
        line.find(' ').map_or_else(
            || Cow::Owned(format!("\x1b[1;36m{line}\x1b[0m")),
            |first_space| {
                let (cmd, args) = line.split_at(first_space);
                Cow::Owned(format!("\x1b[1;36m{cmd}\x1b[0m{args}"))
            },
        )
    }

    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
        Cow::Owned(format!("\x1b[90m{hint}\x1b[0m"))
    }
}
impl Hinter for PumpkinCommandCompleter {
    type Hint = String;
    fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
        if line.is_empty() || pos < line.len() {
            return None;
        }

        if let Ok((_, candidates)) = self.complete(line, pos, ctx)
            && let Some(first) = candidates.first()
        {
            let last_word = line.split_whitespace().last().unwrap_or("");
            if first.starts_with('<') {
                return line.ends_with(' ').then(|| first.clone());
            }

            if let Some(stripped) = first.strip_prefix(last_word) {
                return Some(stripped.to_string());
            }
        }
        None
    }
}

impl Validator for PumpkinCommandCompleter {}

impl Completer for PumpkinCommandCompleter {
    type Candidate = String;

    #[expect(clippy::too_many_lines)]
    fn complete(
        &self,
        line: &str,
        pos: usize,
        _ctx: &rustyline::Context<'_>,
    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
        let cmd_to_cursor = &line[..pos];
        let has_slash = cmd_to_cursor.starts_with('/');
        let cmd = if has_slash {
            &cmd_to_cursor[1..]
        } else {
            cmd_to_cursor
        };

        let Some(handle) = self.rt.get() else {
            return Ok((0, Vec::new()));
        };
        let Ok(server_guard) = self.server.try_read() else {
            return Ok((0, Vec::new()));
        };
        let Some(server) = server_guard.as_ref() else {
            return Ok((0, Vec::new()));
        };

        let parts: Vec<&str> = cmd.split_whitespace().collect();
        let ends_with_space = cmd.ends_with(' ');

        handle.block_on(async {
            let dispatcher = server.command_dispatcher.read().await;
            let source = CommandSender::Console.into_source(server).await;

            // Temporary setups to unify both dispatchers for now:

            {
                if cmd.trim().is_empty() {
                    // Give all commands as suggestions.

                    let suggestions: Vec<String> = dispatcher
                        .get_all_commands()
                        .keys()
                        .map(ToString::to_string)
                        .collect();
                    return Ok((pos, suggestions));
                }
            }

            // Not sure if this is necessary, but I guess we better be safe than sorry.
            if let Some(cursor) = pos.checked_sub(usize::from(has_slash)) {
                let mut reader = StringReader::new(cmd);
                if reader.peek() == Some('/') {
                    reader.skip();
                }
                let parsed = dispatcher.parse(&mut reader, &source).await;
                let suggestions = dispatcher.get_completion_suggestions(parsed, cursor).await;

                if !suggestions.is_empty() {
                    let start = suggestions.range.start;
                    let suggestions = suggestions
                        .suggestions
                        .into_iter()
                        .map(|s| s.text.cached_text().clone())
                        .collect();
                    return Ok((start + usize::from(has_slash), suggestions));
                }
            }

            let dispatcher = &dispatcher.fallback_dispatcher;
            let src = CommandSender::Console;

            if parts.is_empty() || (parts.len() == 1 && !ends_with_space) {
                let typing = parts.first().unwrap_or(&"");
                let candidates = dispatcher
                    .commands
                    .keys()
                    .filter(|k| k.starts_with(typing))
                    .cloned()
                    .collect();
                return Ok((usize::from(has_slash), candidates));
            }

            let Some(tree) = dispatcher.get_tree(parts[0]).ok() else {
                return Ok((0, Vec::new()));
            };

            let mut current_indices = tree.children.clone();
            let mut word_index = 1;
            let walk_limit = if ends_with_space {
                parts.len()
            } else {
                parts.len() - 1
            };

            while word_index < walk_limit {
                let token = parts[word_index];
                let mut next_indices = Vec::new();

                let mut worklist: VecDeque<usize> = current_indices.iter().copied().collect();

                while let Some(idx) = worklist.pop_front() {
                    let node = &tree.nodes[idx];

                    match &node.node_type {
                        NodeType::Require { predicate } => {
                            if predicate(&src) {
                                worklist.extend(node.children.iter().copied());
                            }
                        }
                        NodeType::Literal { string } => {
                            if string.eq_ignore_ascii_case(token) {
                                next_indices.extend(node.children.iter().copied());
                            }
                        }
                        NodeType::Argument { .. } => {
                            next_indices.extend(node.children.iter().copied());
                        }
                        NodeType::ExecuteLeaf { .. } => {}
                    }
                }

                if next_indices.is_empty() {
                    return Ok((0, Vec::new()));
                }

                current_indices = next_indices;
                word_index += 1;
            }

            let typing = if ends_with_space {
                ""
            } else {
                parts.last().unwrap_or(&"")
            };
            let mut candidates = Vec::new();

            let mut suggestion_worklist: VecDeque<usize> = current_indices.into_iter().collect();

            while let Some(idx) = suggestion_worklist.pop_front() {
                let node = &tree.nodes[idx];
                match &node.node_type {
                    NodeType::Require { predicate } => {
                        if predicate(&src) {
                            suggestion_worklist.extend(node.children.iter().copied());
                        }
                    }
                    NodeType::Literal { string } => {
                        if string.starts_with(typing) {
                            candidates.push(string.clone());
                        }
                    }
                    NodeType::Argument { name, consumer } => {
                        let suggest_future = consumer.suggest(&src, server, typing);

                        if let Ok(Some(suggestions)) = suggest_future.await {
                            for s in suggestions {
                                let s = s.suggestion;
                                if s.starts_with(typing) {
                                    candidates.push(s);
                                }
                            }
                        } else {
                            let placeholder = format!("<{name}>");
                            if placeholder.starts_with(typing) || typing.is_empty() {
                                candidates.push(placeholder);
                            }
                        }
                    }
                    NodeType::ExecuteLeaf { executor: _ } => {}
                }
            }

            let last_space = cmd.rfind(' ').map_or(0, |i| i + 1);
            Ok((last_space + usize::from(has_slash), candidates))
        })
    }
}
