// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    fs::read_to_string,
    collections::HashSet,

    path::Path,

    io::{
        self,
        Stdout
    },

    time::{
        Duration,
        Instant
    }
};

use crossterm::{
    execute,

    terminal::{
        EnterAlternateScreen,
        LeaveAlternateScreen,
        disable_raw_mode,
        enable_raw_mode
    }
};

use ratatui::{
    Frame,
    Terminal as RatatuiTerminal,
    backend::CrosstermBackend
};

use super::{
    ui::draw,

    ports::{
        Port,
        Status,
        scan_all
    },

    app::{
        App,
        Mode,
        ToastKind
    },

    repo::{
        Repo,
        RepoPaths
    },

    events::{
        self,
        Event,
        drain_events,
        map_event
    },

    pkg::{
        Action,
        PkgError,
        COMMAND,
        info,
        sync,
        toggle
    }
};

const POLL_INTERVAL:  Duration = Duration::from_millis(200);
const TOAST_DURATION: Duration = Duration::from_secs(2);

pub(super) type Terminal = RatatuiTerminal<CrosstermBackend<Stdout>>;

pub(super) fn load_mask_set(path: &Path) -> HashSet<String> {
    let contents: String = match read_to_string(path) {
        Ok(c) => c,
        Err(_) => return HashSet::new()
    };

    contents.lines().filter_map(|line: &str| {
        let trimmed: &str = line.trim();

        if trimmed.is_empty() || trimmed.starts_with('#') {
            return None;
        }

        Some(trimmed.to_string())
    }).collect()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ToggleOutcome {
    Success,
    Quit
}

#[derive(Debug, thiserror::Error)]
pub(super) enum RuntimeError {
    #[error("terminal error: {}", 0)]
    Terminal(#[from] io::Error)
}

pub(super) struct TerminalGuard {
    term: Terminal,
    active: bool
}

impl TerminalGuard {
    pub(super) fn enter() -> Result<Self, RuntimeError> {
        enable_raw_mode()?;
        execute!(io::stdout(), EnterAlternateScreen)?;

        let backend: CrosstermBackend<Stdout> = CrosstermBackend::new(io::stdout());
        let term: Terminal = RatatuiTerminal::new(backend)?;

        Ok(Self {
            term,
            active: true
        })
    }

    pub(super) fn teardown(&mut self) -> Result<(), RuntimeError> {
        if !self.active {
            return Ok(());
        }

        let _ = execute!(io::stdout(), LeaveAlternateScreen);
        disable_raw_mode()?;

        let _ = (&mut self.term).show_cursor();
        self.active = false;

        Ok(())
    }

    pub(super) fn restore(&mut self) -> Result<(), RuntimeError> {
        if self.active {
            return Ok(());
        }

        execute!(io::stdout(), EnterAlternateScreen)?;
        enable_raw_mode()?;

        (&mut self.term).clear()?;
        let _ = (&mut self.term).hide_cursor();

        self.active = true;
        Ok(())
    }

    pub(super) fn draw<F>(&mut self, f: F) -> Result<(), RuntimeError> where F: FnOnce(&mut Frame) -> () {
        (&mut self.term).draw(f)?;
        Ok(())
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) -> () {
        let _ = (&mut *self).teardown();
    }
}

pub(super) struct Runtime {
    term: TerminalGuard,
    app: App,
    repos: Vec<Repo>,
    paths: RepoPaths,
    mask: HashSet<String>,
    toast_start: Option<Instant>
}

impl Runtime {
    pub(super) fn new(app: App, repos: Vec<Repo>, paths: RepoPaths) -> Result<Self, RuntimeError> {
        let mask: HashSet<String> = load_mask_set(Path::new(super::pkg::MASK_FILE));

        Ok(Self {
            term: TerminalGuard::enter()?,
            app,
            repos,
            paths,
            mask,
            toast_start: None
        })
    }

    fn draw(&mut self) -> Result<(), RuntimeError> {
        (&mut self.term).draw(|f: &mut Frame| draw(f, &self.app))?;
        Ok(())
    }

    fn ensure_preview(&mut self) -> Result<(), RuntimeError> {
        let Some(port) = self.app.selected_port() else {
            self.app.preview_target = None;
            return Ok(());
        };

        if (self.app.preview_target.as_deref() == Some(&port.name)) && self.app.previews.contains_key(&port.name) {
            return Ok(());
        }

        let text: String = info(&port.name);
        (&mut self.app.previews).insert(port.name.clone(), text);

        self.app.preview_target = Some(port.name);
        Ok(())
    }

    fn execute_action(&mut self, ports: Vec<Port>, action: Action) -> Result<ToggleOutcome, RuntimeError> {
        self.app.mode = Mode::Running;
        (&mut self.term).teardown()?;

        let verb: &str = action.verb();
        let mut failed: Vec<(String, PkgError)> = Vec::new();

        for port in &ports {
            let result: Result<(), PkgError> = toggle(port, action);

            if let Err(e) = result {
                (&mut failed).push((port.name.clone(), e));
            }
        }

        let new_status: Status = match action {
            Action::Install | Action::Update | Action::Rebuild => Status::Installed,
            Action::Remove => Status::NotInstalled
        };

        if !failed.is_empty() {
            eprintln!();
            eprintln!("{} {} ({} package(s)): {} FAILED", COMMAND, verb, ports.len(), failed.len());

            for (name, err) in &failed {
                eprintln!("  {}: {}", name, err);
            }

            eprintln!();
            eprintln!("The TUI has exited so you can read the message above.");
            eprintln!("Re-run {} to continue.", env!("CARGO_PKG_NAME"));

            return Ok(ToggleOutcome::Quit);
        }

        (&mut self.term).restore()?;
        self.app.mode = Mode::Browse;

        while events::poll(Duration::ZERO).is_some() {}

        for port in &ports {
            (&mut self.app).mark_toggled(&port.name, new_status);
            (&mut self.app.previews).remove(&port.name);
        }

        let count: usize = ports.len();

        let message: String = if count == 1 {
            format!("{}: {} complete", ports[0].name, verb)
        } else {
            format!("{} packages: {} complete", count, verb)
        };

        (&mut self.app).set_toast(ToastKind::Success, message);
        self.app.preview_target = None;
        self.toast_start = Some(Instant::now());

        (&mut *self).ensure_preview()?;
        Ok(ToggleOutcome::Success)
    }

    fn sync_repos(&mut self) -> Result<(), RuntimeError> {
        self.app.mode = Mode::Running;
        (&mut self.term).teardown()?;

        let sync_result: Result<(), PkgError> = sync();
        (&mut self.term).restore()?;

        while events::poll(Duration::ZERO).is_some() {}

        match sync_result {
            Ok(()) => {
                match scan_all(&self.repos, &self.paths, &self.mask) {
                    Ok(new_ports) => {
                        (&mut self.app).reload_ports(new_ports);
                        (&mut self.app).set_toast(ToastKind::Success, "sync complete — port list refreshed");
                    },

                    Err(e) => (&mut self.app).set_toast(ToastKind::Error, format!("scan failed after sync: {}", e))
                }
            },

            Err(e) => (&mut self.app).set_toast(ToastKind::Error, format!("sync failed: {}", e))
        }

        self.toast_start = Some(Instant::now());
        (&mut *self).ensure_preview()?;

        Ok(())
    }

    pub(super) fn run(&mut self) -> Result<(), RuntimeError> {
        (&mut *self).ensure_preview()?;
        (&mut *self).draw()?;

        while self.app.mode != Mode::Quit {
            if let Some(raw) = drain_events(POLL_INTERVAL) {
                if let Some(ev) = map_event(raw, self.app.mode, !self.app.filter.is_empty()) {
                    (&mut *self).handle_event(ev)?;
                }
            } else if self.app.mode == Mode::Toast {
                if let Some(start) = self.toast_start && (start.elapsed() >= TOAST_DURATION) {
                    (&mut self.app).clear_toast();
                    self.toast_start = None;
                }
            }

            if self.app.mode != Mode::Quit {
                (&mut *self).draw()?;
            }
        }

        Ok(())
    }

    fn handle_event(&mut self, ev: Event) -> Result<(), RuntimeError> {
        match ev {
            Event::FilterChar(c) => {
                (&mut self.app.filter).push(c);
                (&mut *self).scroll_to_selection();
            },

            Event::FilterBackspace => {
                (&mut self.app.filter).pop();
                (&mut *self).scroll_to_selection();
            },

            Event::MoveCursor(d) => (&mut self.app).move_cursor(d),
            Event::Jump(end) => (&mut self.app).jump_to(end),
            Event::OpenActionMenu => (&mut self.app).open_action_menu(),

            Event::ActionMenuMove(d) => {
                if let Some(ref mut menu) = (&mut self.app).action_menu {
                    (&mut *menu).move_selection(d);
                }
            },

            Event::ActionMenuConfirm => {
                if let Some((ports, action)) = (&mut self.app).confirm_action() {
                    (&mut self.app).close_action_menu();
                    (&mut self.app.multi_selection).clear();

                    match (&mut *self).execute_action(ports, action)? {
                        ToggleOutcome::Quit => self.app.mode = Mode::Quit,
                        ToggleOutcome::Success => {}
                    }
                }
            },

            Event::ActionMenuCancel => (&mut self.app).close_action_menu(),

            Event::CycleFilter => {
                (&mut self.app).cycle_status_filter();
                (&mut *self).scroll_to_selection();
            },

            Event::SyncRepos => (&mut *self).sync_repos()?,
            Event::ToggleMultiSelect => (&mut self.app).toggle_multi_select(),
            Event::Quit => self.app.mode = Mode::Quit,

            Event::DismissToast => {
                (&mut self.app).clear_toast();
                self.toast_start = None;
            },

            Event::Redraw => {}
        }

        if matches!(ev, Event::MoveCursor(_) | Event::Jump(_) | Event::FilterChar(_) | Event::FilterBackspace | Event::DismissToast | Event::CycleFilter | Event::ToggleMultiSelect) {
            (&mut *self).ensure_preview()?;
        }

        Ok(())
    }

    fn scroll_to_selection(&mut self) -> () {
        let view: Vec<Port> = self.app.filtered_view();
        let idx: usize = self.app.selected_index();

        if !view.is_empty() {
            self.app.scroll = idx.min(view.len() - 1);
        }
    }
}
