use egui::{Color32, FontId, Frame, RichText, ScrollArea};
use log::{LevelFilter, error, info, warn};
use simplelog::{CombinedLogger, WriteLogger};
use std::{
    fs,
    fs::File,
    marker::PhantomData,
    path::PathBuf,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    thread,
};
use tokio::runtime::Handle;
use tokio_util::sync::CancellationToken;
use url::Url;

use crate::Info;
use crate::crawler::checkpoint::Checkpoint;

mod controls;
mod error;
mod inputs;
mod logging;
pub mod runtime;
pub mod utils;
pub use error::GuiError;
use logging::GuiLogWriter;
pub use runtime::{Runtime, RuntimeConfig};
use utils::scan_checkpoints;

pub struct App<I: Info + Clone + Send + Sync + 'static, R: Runtime<I>> {
    url: String,
    working_directory: String,
    checkpoint_path: Option<String>,
    available_checkpoints: Vec<PathBuf>,
    is_running: Arc<AtomicBool>,
    checkpoints_need_refresh: Arc<AtomicBool>,
    logs: GuiLogWriter,
    runtime_handle: Handle,
    cancel_token: Option<CancellationToken>,
    page_delay_ms: u64,
    active_tab: usize,
    runtime: Arc<R>,
    _marker: PhantomData<I>,
}

impl<I: Info + Clone + Send + Sync + 'static, R: Runtime<I>> App<I, R> {
    /// Create the app. `handle` is the caller's Tokio runtime handle.
    /// `crate_names` filters GUI/file logs to the listed crates.
    pub fn new(
        handle: Handle,
        crate_names: &'static [&'static str],
        runtime: R,
    ) -> Result<Self, GuiError> {
        let logs = GuiLogWriter::new();

        let mut gui_config = simplelog::ConfigBuilder::new();
        gui_config.set_time_level(LevelFilter::Off);
        for crate_name in crate_names {
            let _ = gui_config.add_filter_allow((*crate_name).to_string());
        }
        let gui_config = gui_config.build();
        let mut term_config = simplelog::ConfigBuilder::new();
        let _ = term_config.add_filter_ignore_str("eframe");
        let _ = term_config.add_filter_ignore_str("egui");
        let _ = term_config.add_filter_ignore_str("wgpu");
        let _ = term_config.add_filter_ignore_str("naga");
        let _ = term_config.add_filter_ignore_str("rustls");
        let _ = term_config.add_filter_ignore_str("winit");
        let _ = term_config.add_filter_ignore_str("tracing");
        let _ = term_config.add_filter_ignore_str("tracing::span");
        let term_config = term_config.build();
        if let Err(e) = CombinedLogger::init(vec![
            WriteLogger::new(LevelFilter::Info, gui_config, logs.clone()),
            simplelog::TermLogger::new(
                LevelFilter::Debug,
                term_config,
                simplelog::TerminalMode::Stderr,
                simplelog::ColorChoice::Auto,
            ),
        ]) {
            eprintln!("Failed to initialise logging: {}", e);
        }

        let crate_name = crate_names.first().copied().unwrap_or("crawler");
        let default_output = if cfg!(debug_assertions) {
            "./output".to_string()
        } else {
            dirs::data_dir()
                .map(|d| d.join(crate_name).display().to_string())
                .unwrap_or_else(|| "./output".to_string())
        };

        let available_checkpoints = scan_checkpoints(&default_output);

        Ok(Self {
            logs,
            working_directory: default_output,
            available_checkpoints,
            runtime_handle: handle,
            url: Default::default(),
            checkpoint_path: Default::default(),
            is_running: Arc::new(AtomicBool::new(false)),
            checkpoints_need_refresh: Arc::new(AtomicBool::new(false)),
            cancel_token: Default::default(),
            page_delay_ms: 200,
            active_tab: 0,
            runtime: Arc::new(runtime),
            _marker: PhantomData,
        })
    }
}

impl<I: Info + Clone + Send + Sync + 'static, R: Runtime<I>> eframe::App for App<I, R> {
    fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
        if self.is_running.load(Ordering::Relaxed) {
            info!("Application closing, saving state...");
            self.runtime.on_exit();
        }
    }

    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        self.refresh_checkpoints_if_needed();
        let ctx = ui.ctx().clone();
        ui.heading("🕷️ Crawler");
        ui.separator();
        self.render_tabs(ui);
        ui.add_space(10.0);
        if self.active_tab == 0 {
            self.render_crawler_tab(ui, &ctx);
        } else {
            self.render_extension_tab(ui);
        }
    }
}

impl<I: Info + Clone + Send + Sync + 'static, R: Runtime<I>> App<I, R> {
    fn refresh_checkpoints_if_needed(&mut self) {
        if !self.checkpoints_need_refresh.swap(false, Ordering::Relaxed) {
            return;
        }

        self.available_checkpoints = scan_checkpoints(&self.working_directory);
    }

    fn render_tabs(&mut self, ui: &mut egui::Ui) {
        let tabs = self.runtime.extension_tabs();
        if tabs.is_empty() {
            self.active_tab = 0;
            return;
        }

        ui.horizontal(|ui| {
            if ui
                .selectable_label(self.active_tab == 0, "Crawler")
                .clicked()
            {
                self.active_tab = 0;
            }
            for (index, tab) in tabs.iter().enumerate() {
                let tab_index = index + 1;
                if ui
                    .selectable_label(self.active_tab == tab_index, *tab)
                    .clicked()
                {
                    self.active_tab = tab_index;
                }
            }
        });
        ui.separator();
    }

    fn render_crawler_tab(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) {
        self.render_config_panel(ui);
        ui.add_space(10.0);
        self.render_controls(ui, ctx);
        ui.add_space(10.0);
        self.render_log_panel(ui);
    }

    fn render_extension_tab(&self, ui: &mut egui::Ui) {
        let Some(tab) = self.runtime.extension_tabs().get(self.active_tab - 1) else {
            return;
        };
        self.runtime.render_extension_tab(
            tab,
            ui,
            !self.is_running.load(Ordering::Relaxed),
            &self.runtime_handle,
        );
        ui.add_space(10.0);
        self.render_log_panel(ui);
    }

    fn render_log_panel(&self, ui: &mut egui::Ui) {
        Frame::group(ui.style()).inner_margin(10.0).show(ui, |ui| {
            ui.label(RichText::new("Logs").strong());
            ui.add_space(5.0);

            let available_height = ui.available_height() - 10.0;

            ScrollArea::vertical()
                .max_height(available_height)
                .stick_to_bottom(true)
                .auto_shrink([false, false])
                .show(ui, |ui| {
                    let logs = self.logs.buffer.lock().unwrap();

                    if logs.is_empty() {
                        ui.label(
                            RichText::new("No logs yet. Click 'Start' to begin.")
                                .italics()
                                .color(Color32::DARK_GRAY),
                        );
                    } else {
                        let start_idx = logs.len().saturating_sub(500);
                        for log in logs.iter().skip(start_idx) {
                            render_log_line(ui, log);
                        }
                    }
                });
        });
    }

    pub(super) fn start(&mut self, egui_ctx: egui::Context) -> Result<(), GuiError> {
        let url = self.resolve_start_url()?;
        let path = PathBuf::from(&self.working_directory);

        if let Err(e) = fs::create_dir_all(&path) {
            error!("Failed to create output directory: {e}");
            return Ok(());
        }

        info!("Starting crawler for: {}", url);
        self.is_running.store(true, Ordering::Relaxed);

        let host = url.host_str().unwrap_or("unknown");
        let time_stamp = chrono::Local::now().format("%Y_%m_%d_%H_%M_%S");
        let log_path = path.join(format!("crawler_{host}_{time_stamp}.log"));
        match File::create(&log_path) {
            Ok(f) => {
                self.logs.set_log_file(f);
                info!("📁 Logging to: {}", log_path.display());
            }
            Err(e) => error!("Failed to create log file: {}", e),
        }

        let config = RuntimeConfig {
            url,
            output_path: path,
            resume_from: self.checkpoint_path.as_ref().map(PathBuf::from),
            page_delay_ms: self.page_delay_ms,
        };

        let cancel_token = CancellationToken::new();
        self.cancel_token = Some(cancel_token.clone());

        let handle = self.runtime_handle.clone();
        let runtime = self.runtime.clone();
        let is_running = self.is_running.clone();
        let checkpoints_need_refresh = self.checkpoints_need_refresh.clone();
        let logs = self.logs.clone();

        thread::spawn(move || {
            handle.block_on(async move {
                tokio::select! {
                    result = runtime.run(config, cancel_token.clone()) => {
                        match result {
                            Ok(_) => info!("✓ Completed successfully!"),
                            Err(e) => error!("✗ Failed: {}", e),
                        }
                    }
                    _ = cancel_token.cancelled() => {
                        warn!("Stopped by user");
                    }
                }
                logs.close_log_file();
                is_running.store(false, Ordering::Relaxed);
                checkpoints_need_refresh.store(true, Ordering::Relaxed);
                egui_ctx.request_repaint();
            });
        });
        Ok(())
    }

    fn resolve_start_url(&self) -> Result<Url, GuiError> {
        if !self.url.is_empty() {
            return Url::parse(&self.url).map_err(Into::into);
        }

        if let Some(checkpoint_path) = self.checkpoint_path.as_ref() {
            let checkpoint = Checkpoint::<I>::load(&PathBuf::from(checkpoint_path))?;
            return Ok(checkpoint.start_url);
        }

        if let Some(url) = self.runtime.fallback_start_url() {
            return Ok(url);
        }

        Err(GuiError::Io(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "URL, checkpoint, or runtime input is required",
        )))
    }

    pub(super) fn stop(&mut self) {
        info!("Stopping...");
        if let Some(token) = &self.cancel_token {
            token.cancel();
        }
        self.runtime.on_exit();
        self.logs.close_log_file();
        self.is_running.store(false, Ordering::Relaxed);
        self.checkpoints_need_refresh.store(true, Ordering::Relaxed);
        self.cancel_token = None;
    }
}

fn render_log_line(ui: &mut egui::Ui, log: &str) {
    let (color, icon) = if log.contains("ERROR") {
        (Color32::from_rgb(255, 100, 100), "❌")
    } else if log.contains("WARN") {
        (Color32::from_rgb(255, 200, 0), "⚠️")
    } else if log.contains("INFO") {
        (Color32::from_rgb(200, 200, 200), "ℹ️")
    } else {
        (Color32::LIGHT_GRAY, "")
    };

    ui.horizontal(|ui| {
        if !icon.is_empty() {
            ui.label(icon);
        }
        ui.label(
            RichText::new(log.trim())
                .color(color)
                .font(FontId::monospace(12.0)),
        );
    });
}

#[cfg(test)]
mod tests;
