use crate::ddg::{DdgClient, FetchOutcome, SearchOpts, SearchResult, parse_results};
use crate::output::{Colorize, omniprompt_label, print_json, print_results};
use crate::prompt::{CaptchaResolution, read_omniprompt_line, solve_captcha};
use anyhow::{Context, Result, bail};
use clap::Parser;
use std::process::Command;

#[derive(Parser, Debug)]
#[command(
    name = "ddg",
    version,
    about = "DuckDuckGo from the terminal (Rust port of jarun/ddgr).",
    after_help = "Omniprompt: n=next, p=prev, f=first, <num>=open in browser, q=quit\n"
)]
pub struct Args {
    /// Search keywords
    pub keywords: Vec<String>,

    /// Max results per page (1..=25)
    #[arg(short = 'n', long = "num", default_value_t = 10, value_parser = clap::value_parser!(u8).range(1..=25))]
    pub num: u8,

    /// Region code (e.g. us-en, in-en, wt-wt)
    #[arg(short = 'r', long = "reg", default_value = "us-en")]
    pub region: String,

    /// Time limit: d, w, m, y
    #[arg(short = 't', long = "time")]
    pub time: Option<String>,

    /// Restrict to a site (site: operator)
    #[arg(short = 'w', long = "site")]
    pub site: Option<String>,

    /// Show full URLs in results
    #[arg(short = 'x', long = "expand")]
    pub expand: bool,

    /// HTTPS proxy URI
    #[arg(short = 'p', long = "proxy")]
    pub proxy: Option<String>,

    /// Disable safe search
    #[arg(long = "unsafe")]
    pub unsafe_: bool,

    /// Custom user agent (omit value to disable)
    #[arg(long = "ua")]
    pub user_agent: Option<String>,

    /// JSON output (implies --np)
    #[arg(long = "json")]
    pub json: bool,

    /// Don't enter the omniprompt; just print results and exit
    #[arg(long = "np")]
    pub no_prompt: bool,

    /// Colorize output
    #[arg(long = "colorize", default_value = "auto", value_parser = ["auto", "always", "never"])]
    pub colorize: String,

    /// Disable color (alias for --colorize=never)
    #[arg(short = 'C', long = "nocolor")]
    pub nocolor: bool,

    /// On CAPTCHA, auto-open the tile contact sheet in the default browser.
    /// Off by default to avoid spawning many tabs across parallel queries.
    #[arg(long = "open-captcha")]
    pub open_captcha: bool,
}

pub fn run() -> Result<()> {
    let args = Args::parse();

    if args.keywords.is_empty() {
        bail!("no search keywords; try `ddg rust traits` or --help");
    }

    let query = args.keywords.join(" ");
    let safe: i8 = if args.unsafe_ { -1 } else { 1 };
    let opts = SearchOpts {
        query: &query,
        region: &args.region,
        time: args.time.as_deref(),
        safe,
        site: args.site.as_deref(),
    };

    let client = DdgClient::new(args.proxy.as_deref(), args.user_agent.as_deref())?;

    let color = if args.nocolor || args.colorize == "never" {
        Colorize::Never
    } else if args.colorize == "always" {
        Colorize::Always
    } else {
        Colorize::Auto
    };

    let mut state = PageState::default();
    fetch_first_with_captcha(&client, &opts, &mut state, args.open_captcha)?;

    render_page(&state, args.num as usize, args.expand, args.json, &color)?;

    if args.json || args.no_prompt {
        return Ok(());
    }

    omniprompt(&client, &opts, &args, &mut state, &color)
}

#[derive(Default)]
struct PageState {
    page: usize,        // 0 == first page
    cur_index: usize,   // running offset of total results fetched
    next_params: Option<String>,
    prev_params: Option<String>,
    vqd: Option<String>,
    results: Vec<SearchResult>,
}

fn fetch_first_with_captcha(
    client: &DdgClient,
    opts: &SearchOpts<'_>,
    state: &mut PageState,
    auto_open: bool,
) -> Result<()> {
    let html = match client.fetch_first(opts)? {
        FetchOutcome::Ok(html) => html,
        FetchOutcome::Captcha { body, status, url } => {
            match solve_captcha(client, &body, status, &url, auto_open)? {
                CaptchaResolution::Resolved(html) => html,
                CaptchaResolution::Abort => bail!("aborted at CAPTCHA challenge"),
            }
        }
    };
    let parsed = parse_results(&html, state.cur_index);
    state.cur_index += parsed.results.len();
    state.next_params = parsed.next_params;
    state.prev_params = parsed.prev_params;
    state.vqd = parsed.vqd;
    state.results = parsed.results;
    state.page = 0;
    Ok(())
}

fn fetch_next_with_captcha(
    client: &DdgClient,
    opts: &SearchOpts<'_>,
    state: &mut PageState,
    auto_open: bool,
) -> Result<()> {
    let np = state
        .next_params
        .clone()
        .context("no next-page token (last page reached or page 1 of empty results)")?;
    let vqd = state.vqd.clone().unwrap_or_default();
    let next_page = state.page + 1;

    let html = match client.fetch_next(opts, next_page, state.cur_index, &np, &vqd)? {
        FetchOutcome::Ok(html) => html,
        FetchOutcome::Captcha { body, status, url } => {
            match solve_captcha(client, &body, status, &url, auto_open)? {
                CaptchaResolution::Resolved(html) => html,
                CaptchaResolution::Abort => bail!("aborted at CAPTCHA challenge"),
            }
        }
    };
    let parsed = parse_results(&html, state.cur_index);
    state.cur_index += parsed.results.len();
    state.next_params = parsed.next_params;
    state.prev_params = parsed.prev_params;
    if parsed.vqd.is_some() {
        state.vqd = parsed.vqd;
    }
    state.results = parsed.results;
    state.page = next_page;
    Ok(())
}

fn render_page(
    state: &PageState,
    n: usize,
    expand: bool,
    json: bool,
    color: &Colorize,
) -> Result<()> {
    let trimmed: Vec<SearchResult> = state.results.iter().take(n).cloned().collect();
    if json {
        print_json(&trimmed)?;
    } else {
        print_results(&trimmed, expand, color);
    }
    Ok(())
}

fn omniprompt(
    client: &DdgClient,
    opts: &SearchOpts<'_>,
    args: &Args,
    state: &mut PageState,
    color: &Colorize,
) -> Result<()> {
    let prompt = omniprompt_label("ddg", color);
    loop {
        let line = read_omniprompt_line(&prompt)?;
        if line.is_empty() {
            continue;
        }
        match line.as_str() {
            "q" | "quit" | "exit" => return Ok(()),
            "?" | "help" => print_help(),
            "n" => {
                if let Err(e) = fetch_next_with_captcha(client, opts, state, args.open_captcha) {
                    eprintln!("next: {e:#}");
                    continue;
                }
                render_page(state, args.num as usize, args.expand, false, color)?;
            }
            "p" | "f" => {
                eprintln!(
                    "(prev/first not implemented yet — most users just re-run the query)"
                );
            }
            "x" => {
                let new_expand = !args.expand;
                eprintln!("(expand={new_expand}, re-rendering current page)");
                let trimmed: Vec<SearchResult> =
                    state.results.iter().take(args.num as usize).cloned().collect();
                print_results(&trimmed, new_expand, color);
            }
            other => {
                if let Ok(idx) = other.parse::<usize>() {
                    open_index(state, idx)?;
                } else {
                    eprintln!("(unknown command: {other:?} — try `?`)");
                }
            }
        }
    }
}

fn open_index(state: &PageState, idx: usize) -> Result<()> {
    let Some(r) = state.results.iter().find(|r| r.index == idx) else {
        eprintln!("no result at index {idx}");
        return Ok(());
    };
    eprintln!("opening: {}", r.url);
    let prog: &str;
    let mut cmd: Command;
    #[cfg(target_os = "macos")]
    {
        prog = "open";
        cmd = Command::new(prog);
        cmd.arg(&r.url);
    }
    #[cfg(target_os = "linux")]
    {
        prog = "xdg-open";
        cmd = Command::new(prog);
        cmd.arg(&r.url);
    }
    #[cfg(target_os = "windows")]
    {
        prog = "cmd";
        cmd = Command::new(prog);
        cmd.args(["/C", "start", "", &r.url]);
    }
    let _ = cmd.status().with_context(|| format!("spawn {prog}"))?;
    Ok(())
}

fn print_help() {
    eprintln!(
        "  n           next page
  <num>       open result <num> in browser
  x           toggle URL expansion (current page)
  ?           help
  q, Ctrl-D   quit"
    );
}
