use crate::captcha::{build_answer_form, download_tiles, parse_challenge};
use crate::ddg::{DdgClient, FetchOutcome};
use anyhow::{Context, Result};
use std::io::{self, BufRead, IsTerminal, Write};
use std::path::Path;
use std::process::Command;

pub enum CaptchaResolution {
    /// Got a real results page; here it is.
    Resolved(String),
    /// User chose to abort.
    Abort,
}

/// Drive the full CAPTCHA flow: parse the challenge, download tiles, prompt
/// the user for which squares contain a duck, POST the answer, return the
/// resulting body (or another CAPTCHA, in which case retry).
///
/// `auto_open` controls whether we shell out to the OS to open the contact
/// sheet — default off, since opening many tabs at once during a burst of
/// parallel queries is annoying.
pub fn solve_captcha(
    client: &DdgClient,
    body: &str,
    status: u16,
    response_url: &str,
    auto_open: bool,
) -> Result<CaptchaResolution> {
    let mut current_body = body.to_string();
    let mut current_url = response_url.to_string();
    let mut current_status = status;

    for attempt in 1..=3 {
        eprintln!();
        eprintln!("---");
        eprintln!(
            "DuckDuckGo CAPTCHA challenge (HTTP {current_status}) — attempt {attempt}/3"
        );

        let challenge = match parse_challenge(&current_body, &current_url) {
            Ok(c) => c,
            Err(e) => {
                eprintln!("could not parse challenge form: {e:#}");
                eprintln!("(this may be a Cloudflare JS challenge — falling back to browser hand-off)");
                return browser_fallback(&current_body);
            }
        };

        let dir = download_tiles(client.http(), client.user_agent(), &challenge)
            .context("download captcha tiles")?;
        let sheet = dir.join("index.html");
        let sheet_url = format!("file://{}", sheet.display());

        eprintln!("Tiles downloaded to: {}", dir.display());
        eprintln!("Contact sheet:       {sheet_url}");
        if auto_open {
            eprintln!("Opening contact sheet in your browser…");
            if let Err(e) = open_path(&sheet_url) {
                eprintln!("(could not auto-open: {e}; open the URL above manually)");
            }
        } else {
            eprintln!("(open the URL above in a browser to view the tiles; pass --open-captcha to auto-open)");
        }

        let prompt = "\nEnter indices (0..8) of squares containing a duck (space-separated),\n  or 'q' to abort, or 's' to skip / submit none: ";
        let line = match read_line_or_file(prompt, &dir)? {
            Some(s) => s,
            None => {
                eprintln!("(no input — aborting)");
                return Ok(CaptchaResolution::Abort);
            }
        };

        let trimmed = line.trim();
        if matches!(trimmed, "q" | "quit" | "abort") {
            return Ok(CaptchaResolution::Abort);
        }
        let selected: Vec<usize> = if trimmed == "s" || trimmed.is_empty() {
            Vec::new()
        } else {
            match parse_indices(trimmed, challenge.tiles.len()) {
                Ok(v) => v,
                Err(e) => {
                    eprintln!("invalid input ({e}); try again.");
                    continue;
                }
            }
        };

        eprintln!(
            "Submitting answer with {} tiles selected…",
            selected.len()
        );
        let form = build_answer_form(&challenge, &selected);
        match client.submit_captcha_answer(&challenge.action_url, &form)? {
            FetchOutcome::Ok(body) => {
                eprintln!("CAPTCHA solved.");
                return Ok(CaptchaResolution::Resolved(body));
            }
            FetchOutcome::Captcha {
                body,
                status,
                url,
            } => {
                eprintln!("Still challenged — DDG served another CAPTCHA.");
                current_body = body;
                current_url = url;
                current_status = status;
            }
        }
    }

    eprintln!("Giving up after 3 CAPTCHA attempts.");
    Ok(CaptchaResolution::Abort)
}

fn browser_fallback(body: &str) -> Result<CaptchaResolution> {
    let path = save_html(body)?;
    let file_url = format!("file://{}", path.display());
    eprintln!("Saved page: {file_url}");
    eprintln!("Open it in a browser, solve the challenge there, then re-run your search.");
    Ok(CaptchaResolution::Abort)
}

fn save_html(body: &str) -> Result<std::path::PathBuf> {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let path = std::env::temp_dir().join(format!("ddg-rs-challenge-{nanos}.html"));
    std::fs::write(&path, body)?;
    Ok(path)
}

fn parse_indices(s: &str, n_tiles: usize) -> Result<Vec<usize>> {
    let mut out = Vec::new();
    for tok in s.split(|c: char| c.is_whitespace() || c == ',') {
        if tok.is_empty() {
            continue;
        }
        let i: usize = tok.parse().with_context(|| format!("not a number: {tok:?}"))?;
        if i >= n_tiles {
            anyhow::bail!("index {i} out of range (0..{})", n_tiles - 1);
        }
        if !out.contains(&i) {
            out.push(i);
        }
    }
    Ok(out)
}

fn open_path(url: &str) -> Result<()> {
    #[cfg(target_os = "macos")]
    let mut cmd = {
        let mut c = Command::new("open");
        c.arg(url);
        c
    };
    #[cfg(target_os = "linux")]
    let mut cmd = {
        let mut c = Command::new("xdg-open");
        c.arg(url);
        c
    };
    #[cfg(target_os = "windows")]
    let mut cmd = {
        let mut c = Command::new("cmd");
        c.args(["/C", "start", "", url]);
        c
    };
    let status = cmd.status()?;
    if !status.success() {
        anyhow::bail!("opener exited with {status}");
    }
    Ok(())
}

/// Read one line of user input. Returns `None` on EOF (so callers don't
/// busy-loop when stdin is closed/piped).
pub fn read_line(prompt: &str) -> Result<Option<String>> {
    if !io::stdin().is_terminal() {
        // No interactive stdin; can't safely prompt.
        eprintln!("{prompt}");
        eprintln!("(stdin is not a terminal — cannot prompt interactively)");
        return Ok(None);
    }
    eprint!("{prompt}");
    io::stderr().flush().ok();
    io::stdout().flush().ok();
    let mut line = String::new();
    let n = io::stdin().lock().read_line(&mut line)?;
    if n == 0 {
        return Ok(None); // EOF (Ctrl-D)
    }
    Ok(Some(line))
}

/// Like `read_line`, but on a non-TTY stdin falls back to polling
/// `<dir>/answer.txt` for up to 5 minutes (1s ticks). Lets external scripts
/// (or a human in another terminal) feed the captcha answer to a daemonised
/// search.
pub fn read_line_or_file(prompt: &str, dir: &Path) -> Result<Option<String>> {
    if io::stdin().is_terminal() {
        return read_line(prompt);
    }
    let answer_path = dir.join("answer.txt");
    eprintln!("{prompt}");
    eprintln!(
        "(stdin is not a TTY — write the answer to {} to proceed; waits up to 5 min)",
        answer_path.display()
    );
    io::stderr().flush().ok();
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(300);
    while std::time::Instant::now() < deadline {
        if let Ok(s) = std::fs::read_to_string(&answer_path) {
            let s = s.trim().to_string();
            if !s.is_empty() {
                eprintln!("(read answer from {}: {:?})", answer_path.display(), s);
                return Ok(Some(s));
            }
        }
        std::thread::sleep(std::time::Duration::from_secs(1));
    }
    eprintln!("(timed out waiting for {})", answer_path.display());
    Ok(None)
}

pub fn read_omniprompt_line(prompt: &str) -> Result<String> {
    match read_line(prompt)? {
        Some(s) => Ok(s.trim().to_string()),
        None => Ok("q".to_string()),
    }
}

