use crate::browser::{Browser, LoadedPage};
use crate::storage::ProfileStore;
use crate::terminal::terminal_safe_text;
use std::io::{self, BufRead, Write};
use textwrap::{Options, fill};

pub const MAX_TERMINAL_COMMAND_LINE_BYTES: usize = 64 * 1024;

#[derive(Debug, PartialEq, Eq)]
enum CommandLineRead {
    Eof,
    Line(String),
    TooLong,
}

pub fn run_interactive(
    mut browser: Browser,
    store: ProfileStore,
    start: Option<String>,
) -> anyhow::Result<()> {
    print_banner(store.path().display().to_string().as_str());

    if let Some(start) = start {
        match browser.open(&start) {
            Ok(page) => print_page(page),
            Err(err) => eprintln!("open failed: {err:#}"),
        }
    }

    let stdin = io::stdin();
    let mut stdin = stdin.lock();
    loop {
        print!(
            "{} ",
            style_text("cephas>", "1;36", terminal_colors_enabled())
        );
        io::stdout().flush()?;

        let line = match read_bounded_command_line(&mut stdin, MAX_TERMINAL_COMMAND_LINE_BYTES)? {
            CommandLineRead::Eof => {
                store.save(&browser.profile)?;
                return Ok(());
            }
            CommandLineRead::Line(line) => line,
            CommandLineRead::TooLong => {
                eprintln!(
                    "command line exceeds {} bytes; input discarded",
                    MAX_TERMINAL_COMMAND_LINE_BYTES
                );
                continue;
            }
        };

        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        let mut parts = line.splitn(2, char::is_whitespace);
        let command = parts.next().unwrap_or_default();
        let rest = parts.next().unwrap_or_default().trim();

        let result = match command {
            "open" | "o" => require_arg(rest, "open <url-or-search>").and_then(|target| {
                browser.open(target).map(|page| {
                    print_page(page);
                    true
                })
            }),
            "home" => {
                let home = browser.profile.home.to_string();
                browser.open(&home).map(|page| {
                    print_page(page);
                    true
                })
            }
            "reload" | "r" => browser.reload().map(|page| {
                print_page(page);
                true
            }),
            "back" | "b" => browser.back().map(|page| {
                if let Some(page) = page {
                    print_page(page);
                } else {
                    println!("no back history");
                }
                true
            }),
            "forward" | "f" => browser.forward().map(|page| {
                if let Some(page) = page {
                    print_page(page);
                } else {
                    println!("no forward history");
                }
                true
            }),
            "read" => {
                if let Some(page) = &browser.current {
                    print_reader(page);
                } else {
                    println!("no page is currently open");
                }
                Ok(false)
            }
            "where" | "url" => {
                if let Some(page) = &browser.current {
                    println!("{}", page.url);
                } else {
                    println!("no page is currently open");
                }
                Ok(false)
            }
            "privacy" | "armour" | "armor" | "shields" => {
                if rest.eq_ignore_ascii_case("off") {
                    browser.set_shields_for_current_site(false)?;
                    println!("armour disabled for current site");
                    Ok(true)
                } else if rest.eq_ignore_ascii_case("on") {
                    browser.set_shields_for_current_site(true)?;
                    println!("armour enabled for current site");
                    Ok(true)
                } else {
                    print_privacy(&browser);
                    Ok(false)
                }
            }
            "bookmark" | "mark" => browser.bookmark_current().map(|added| {
                if added {
                    println!("bookmark added");
                } else {
                    println!("bookmark already exists");
                }
                true
            }),
            "bookmarks" | "marks" => {
                for (index, bookmark) in browser.profile.bookmarks.iter().enumerate() {
                    println!(
                        "{}. {} - {}",
                        index + 1,
                        terminal_safe_text(&bookmark.title),
                        bookmark.url
                    );
                }
                Ok(false)
            }
            "history" => {
                for entry in browser.profile.history.iter().rev().take(25) {
                    println!(
                        "{} - {} - {}",
                        entry.visited_at.format("%Y-%m-%d %H:%M"),
                        terminal_safe_text(&entry.title),
                        entry.url
                    );
                }
                Ok(false)
            }
            "diagnostics" | "diag" | "memory" => {
                print_diagnostics(&browser);
                Ok(false)
            }
            "forget" | "shred" => browser.forget_current_site().map(|removed| {
                println!("removed {removed} history entries for current site");
                true
            }),
            "save" => {
                store.save(&browser.profile)?;
                println!("saved profile to {}", store.path().display());
                Ok(false)
            }
            "help" | "?" => {
                print_help();
                Ok(false)
            }
            "quit" | "q" | "exit" => {
                store.save(&browser.profile)?;
                return Ok(());
            }
            _ => browser.open(line).map(|page| {
                print_page(page);
                true
            }),
        };

        match result {
            Ok(should_save) => {
                if should_save {
                    store.save(&browser.profile)?;
                }
            }
            Err(err) => eprintln!("{err:#}"),
        }
    }
}

pub fn print_page(page: &LoadedPage) {
    println!();
    println!("{}", style_text("Page", "1;34", terminal_colors_enabled()));
    println!("  {}", terminal_safe_text(&page.article.title));
    println!("  {} [{}]", page.url, page.status);

    if page.privacy.has_actions() {
        print_report(page);
    }

    println!();
    print_reader(page);
}

pub fn print_reader(page: &LoadedPage) {
    let options = Options::new(100).break_words(false);
    if let Some(excerpt) = &page.article.excerpt {
        println!("{}", fill(&terminal_safe_text(excerpt), &options));
        println!();
    }

    let text = if page.article.text.is_empty() {
        "No readable article text found for this page.".to_string()
    } else {
        terminal_safe_text(&page.article.text)
    };

    for paragraph in text
        .split('\n')
        .filter(|line| !line.trim().is_empty())
        .take(30)
    {
        println!("{}", fill(paragraph, &options));
        println!();
    }
}

fn print_report(page: &LoadedPage) {
    let report = &page.privacy;
    println!(
        "{}",
        style_text("Privacy report", "1;35", terminal_colors_enabled())
    );
    if report.upgraded_to_https {
        println!("  + upgraded to HTTPS");
    }
    if !report.stripped_query_params.is_empty() {
        println!(
            "  + stripped query params: {}",
            terminal_safe_text(&report.stripped_query_params.join(", "))
        );
    }
    if let Some(target) = &report.debounced_redirect {
        println!("  + bypassed redirector to {}", terminal_safe_text(target));
    }
    if let Some(target) = &report.de_amped_to {
        println!("  + redirected AMP page to {}", terminal_safe_text(target));
    }
    if !report.blocked_requests.is_empty() {
        println!(
            "  + blocked {} tracker-like subresources",
            report.blocked_requests.len()
        );
    }
}

fn print_privacy(browser: &Browser) {
    let shields = &browser.profile.shields;
    println!(
        "{}",
        style_text("Armour", "1;35", terminal_colors_enabled())
    );
    println!("  armour: {}", if shields.enabled { "on" } else { "off" });
    println!("  https upgrade: {}", on_off(shields.https_upgrade));
    println!(
        "  global privacy control: {}",
        on_off(shields.global_privacy_control)
    );
    println!("  do not track: {}", on_off(shields.do_not_track));
    println!(
        "  query stripping: {}",
        on_off(shields.strip_tracking_queries)
    );
    println!(
        "  redirect debouncing: {}",
        on_off(shields.debounce_redirects)
    );
    println!("  tracker blocking: {}", on_off(shields.block_trackers));
    println!("  de-amp: {}", on_off(shields.de_amp));

    if let Some(page) = &browser.current {
        println!(
            "  current site protected: {}",
            on_off(shields.is_enabled_for(&page.url))
        );
        print_report(page);
    }
}

fn print_diagnostics(browser: &Browser) {
    let diagnostics = browser.diagnostics();
    println!(
        "{}",
        style_text("Diagnostics", "1;34", terminal_colors_enabled())
    );
    println!(
        "  navigation      back {} / forward {} / max {}",
        diagnostics.back_stack_entries,
        diagnostics.forward_stack_entries,
        diagnostics.max_navigation_stack
    );
    println!(
        "  history         {} / {}",
        diagnostics.history_entries, diagnostics.max_history_entries
    );
    println!(
        "  bookmarks       {} / {}",
        diagnostics.bookmark_entries, diagnostics.max_bookmark_entries
    );
    println!(
        "  page body bytes {} / {}",
        diagnostics.current_body_bytes, diagnostics.max_page_body_bytes
    );
}

fn print_banner(profile_path: &str) {
    let colors = terminal_colors_enabled();
    println!("{}", style_text("Cephas", "1;36", colors));
    println!("  privacy-first Rust browser");
    println!("  profile: {profile_path}");
    println!("  type help for commands; type any URL or search directly to browse");
}

fn print_help() {
    println!(
        "{}",
        style_text("Commands", "1;34", terminal_colors_enabled())
    );
    for (command, description) in [
        ("open <url/search>", "open a URL or search terms"),
        ("back | forward", "navigate history stack"),
        ("reload", "reload current page"),
        ("read", "show reader-mode text again"),
        ("privacy", "show armour report"),
        ("armour on/off", "enable or disable armour for current site"),
        ("bookmark", "bookmark current page"),
        ("bookmarks", "list bookmarks"),
        ("history", "list recent history"),
        ("diagnostics", "show bounded resource counts"),
        ("forget", "delete history for current site"),
        ("save", "save profile"),
        ("quit", "save and exit"),
    ] {
        println!("  {command:<18} {description}");
    }
}

fn require_arg<'a>(value: &'a str, usage: &str) -> anyhow::Result<&'a str> {
    if value.is_empty() {
        anyhow::bail!("usage: {usage}");
    }
    Ok(value)
}

fn read_bounded_command_line<R: BufRead>(
    reader: &mut R,
    max_bytes: usize,
) -> anyhow::Result<CommandLineRead> {
    let mut bytes = Vec::new();
    let mut too_long = false;

    loop {
        let buffer = reader.fill_buf()?;
        if buffer.is_empty() {
            if bytes.is_empty() {
                return Ok(CommandLineRead::Eof);
            }
            break;
        }

        let newline_position = buffer.iter().position(|byte| *byte == b'\n');
        let data_len = newline_position.unwrap_or(buffer.len());
        let chunk_len = newline_position.map_or(buffer.len(), |position| position + 1);
        let remaining = max_bytes.saturating_sub(bytes.len());

        if remaining > 0 {
            bytes.extend_from_slice(&buffer[..data_len.min(remaining)]);
        }
        if data_len > remaining {
            too_long = true;
        }

        reader.consume(chunk_len);
        if newline_position.is_some() {
            break;
        }
    }

    if too_long {
        Ok(CommandLineRead::TooLong)
    } else {
        Ok(CommandLineRead::Line(
            String::from_utf8_lossy(&bytes).into_owned(),
        ))
    }
}

fn on_off(value: bool) -> &'static str {
    if value { "on" } else { "off" }
}

fn terminal_colors_enabled() -> bool {
    std::env::var_os("NO_COLOR").is_none()
        && std::env::var("TERM").map_or(true, |term| term != "dumb")
}

fn style_text(text: &str, code: &str, enabled: bool) -> String {
    if enabled {
        format!("\x1b[{code}m{text}\x1b[0m")
    } else {
        text.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn style_text_respects_color_flag() {
        assert_eq!(style_text("Cephas", "1;36", false), "Cephas");
        assert_eq!(
            style_text("Cephas", "1;36", true),
            "\x1b[1;36mCephas\x1b[0m"
        );
    }

    #[test]
    fn bounded_command_line_reads_normal_input_and_eof() {
        let mut input = std::io::Cursor::new(b"open https://example.com\n");
        assert_eq!(
            read_bounded_command_line(&mut input, 64).unwrap(),
            CommandLineRead::Line("open https://example.com".to_string())
        );
        assert_eq!(
            read_bounded_command_line(&mut input, 64).unwrap(),
            CommandLineRead::Eof
        );
    }

    #[test]
    fn bounded_command_line_discards_oversized_line() {
        let mut input = std::io::Cursor::new(b"abcdef\nnext\n");
        assert_eq!(
            read_bounded_command_line(&mut input, 3).unwrap(),
            CommandLineRead::TooLong
        );
        assert_eq!(
            read_bounded_command_line(&mut input, 8).unwrap(),
            CommandLineRead::Line("next".to_string())
        );
    }
}
