use std::fs;
use std::io::{self, Write};
use std::process;

use bpaf::{OptionParser, Parser, construct, long, positional, short};
use gonidium::opt::const_fold::const_fold;
use gonidium::{
    DiagConfig, ReplConfig, ReplSession, Severity, Value, format_params_signature,
    format_value_with_dtype, interp_eval, load_effective_config_from_cwd,
    lower_with_config_collect, optimize_with_info_and_config, parse_dsl_with_errors,
    render_simplified_expr,
};

#[derive(Debug, Clone)]
pub struct Run {
    pub no_opt: bool,
    pub inputs: Option<String>,
    pub expr: Option<String>,
    pub file: Option<String>,
}

#[derive(Debug, Clone, Default)]
pub struct Repl {
    pub no_opt: bool,
}

#[derive(Debug, Clone)]
pub enum Command {
    Run(Run),
    Repl(Repl),
    // Format
    // Check
}

fn run_parser() -> impl Parser<Command> {
    let no_opt = long("no-opt")
        .help("Skip optimization (default: run optimize).")
        .switch();
    let inputs = long("inputs")
        .help("Comma-separated input values (required when inputs are needed for evaluation).")
        .argument("LIST")
        .optional();
    let expr = short('e')
        .long("expr")
        .help("Inline source code. Otherwise SOURCE is a file path.")
        .argument("CODE")
        .optional();
    let file = positional("FILE")
        .help("Source file path (use -e for inline code).")
        .optional();
    construct!(Run {
        no_opt,
        inputs,
        expr,
        file
    })
    .map(Command::Run)
}

fn repl_parser() -> impl Parser<Command> {
    let no_opt = long("no-opt")
        .help("Skip optimization in REPL (overrides config).")
        .switch();
    construct!(Repl { no_opt }).map(Command::Repl)
}

fn cli_parser() -> OptionParser<Command> {
    let run_cmd = run_parser().to_options().command("run");
    let repl_cmd = repl_parser().to_options().command("repl");
    construct!([run_cmd, repl_cmd]).to_options()
}

fn parse_inputs(s: &str, expected_len: usize) -> Result<Vec<Value>, String> {
    let trimmed = s.trim();
    if trimmed.is_empty() {
        if expected_len == 0 {
            return Ok(Vec::new());
        } else {
            return Err(format!("expected {} input(s), got 0", expected_len));
        }
    }

    let parts: Vec<&str> = trimmed.split(',').map(|p| p.trim()).collect();
    if parts.len() != expected_len {
        return Err(format!(
            "expected {} input(s), got {}",
            expected_len,
            parts.len()
        ));
    }
    let mut out = Vec::with_capacity(parts.len());
    for p in parts {
        let v = if p == "true" {
            Value::Bool(true)
        } else if p == "false" {
            Value::Bool(false)
        } else if let Ok(i) = p.parse::<i64>() {
            Value::SInt { bits: 64, value: i }
        } else if let Ok(f) = p.parse::<f64>() {
            Value::F64(f)
        } else {
            return Err(format!("invalid input value: {}", p));
        };
        out.push(v);
    }
    Ok(out)
}

fn run_command(run: Run) {
    let Run {
        no_opt,
        inputs,
        expr,
        file,
    } = run;
    let loaded = load_effective_config_from_cwd();
    let diag_cfg: DiagConfig = loaded.diag;

    let (source_id, source) = match (&expr, &file) {
        (Some(code), None) => ("<expr>".to_string(), code.clone()),
        (None, Some(path)) => {
            let src = fs::read_to_string(path).unwrap_or_else(|e| {
                eprintln!("gonidium: read {}: {}", path, e);
                process::exit(1);
            });
            (path.clone(), src)
        }
        (None, None) => {
            eprintln!("gonidium: provide either -e <CODE> or <FILE>");
            process::exit(1);
        }
        (Some(_), Some(_)) => {
            eprintln!("gonidium: only one SOURCE (file or -e) allowed");
            process::exit(1);
        }
    };

    let func = match parse_dsl_with_errors(&source) {
        Ok(f) => f,
        Err(diags) => {
            for d in diags {
                eprintln!("{}", d.report_string_default(&source_id, &source));
            }
            process::exit(1);
        }
    };

    let lower_result = lower_with_config_collect(&func, diag_cfg.clone());
    for d in &lower_result.diags {
        let sev = diag_cfg.severity(d.code);
        if sev >= Severity::Info {
            eprintln!("{}", d.report_string(&source_id, &source, sev));
        }
    }
    for e in &lower_result.errors {
        eprintln!("{}", e.report_string(&source_id, &source, Severity::Error));
    }
    if !lower_result.errors.is_empty() {
        process::exit(1);
    }

    // Keep a copy of the lowered DAG (with explicit-cast marks) for pretty-printing,
    // but run optimisation on a separate copy used for execution. For printing we
    // additionally run a const-fold pass to simplify literals.
    let lowered_dag = lower_result.dag;
    let display_dag = const_fold(&lowered_dag).dag;
    let mut dag = lowered_dag.clone();
    if !no_opt && let Ok(opt) = optimize_with_info_and_config(&dag, &loaded.opt) {
        let result_span = (func.body.span.start, func.body.span.end);
        for d in &opt.diags {
            let sev = diag_cfg.severity(d.code);
            if sev >= Severity::Info {
                let mut d = d.clone();
                if d.span.is_none() {
                    d.span = Some(result_span);
                }
                eprintln!("{}", d.report_string(&source_id, &source, sev));
            }
        }
        dag = opt.dag;
    }

    let expected_inputs = dag.params.len();
    let inputs_str = inputs.as_deref().unwrap_or("").trim();

    if expected_inputs > 0 && inputs_str.is_empty() {
        // No inputs: first line |params| -> dtype, second line(s) expression(s) only.
        // Use the const-folded lowered DAG for rendering so that only explicit casts
        // are shown, and literals are simplified.
        let params_sig = format_params_signature(&display_dag.params);
        let return_type = if display_dag.outputs.len() == 1 {
            display_dag.dtypes[display_dag.outputs[0].0]
                .to_str()
                .to_string()
        } else {
            let dtypes: Vec<_> = display_dag
                .outputs
                .iter()
                .map(|&id| display_dag.dtypes[id.0].to_str())
                .collect();
            format!("({})", dtypes.join(", "))
        };
        println!("{} -> {}", params_sig, return_type);
        for (i, &out_id) in display_dag.outputs.iter().enumerate() {
            let expr = render_simplified_expr(&display_dag, out_id, true);
            if display_dag.outputs.len() > 1 {
                println!("out[{}] = {}", i, expr);
            } else {
                println!("{}", expr);
            }
        }
        return;
    }

    let inputs = parse_inputs(inputs_str, expected_inputs).unwrap_or_else(|e| {
        eprintln!("gonidium: {}", e);
        process::exit(1);
    });

    match interp_eval(&dag, &inputs) {
        Ok(results) => {
            for (i, v) in results.iter().enumerate() {
                let out_id = dag
                    .outputs
                    .get(i)
                    .copied()
                    .unwrap_or_else(|| panic!("missing output for result index {i}"));
                let dtype = dag.dtypes[out_id.0];
                if results.len() > 1 {
                    print!("out[{}] = ", i);
                }
                println!("{}", format_value_with_dtype(v, dtype));
            }
        }
        Err(e) => {
            eprintln!("interp error: {}", e);
            process::exit(1);
        }
    }
}

/// Line-based REPL using rustyline (IPython-style).
fn repl_line_mode(session: &mut ReplSession) {
    let mut rl = match rustyline::DefaultEditor::new() {
        Ok(ed) => ed,
        Err(e) => {
            eprintln!("gonidium: repl init error: {}", e);
            process::exit(1);
        }
    };
    let mut stdout = io::stdout();

    // Print the initial header so the user sees the starting state.
    let header = session.header();
    let _ = writeln!(stdout, "{}", header.inputs_summary);
    let _ = stdout.flush();

    loop {
        let line = match rl.readline(">> ") {
            Ok(s) => s,
            Err(rustyline::error::ReadlineError::Eof) => break,
            Err(rustyline::error::ReadlineError::Interrupted) => continue,
            Err(e) => {
                eprintln!("gonidium: repl read error: {}", e);
                process::exit(1);
            }
        };

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

        // REPL commands (IPython-style)
        if trimmed.starts_with(':') {
            match trimmed {
                ":q" | ":quit" => break,
                ":i" | ":inputs" => {
                    let header = session.header();
                    let _ = writeln!(stdout, "{}", header.inputs_summary);
                }
                ":l" | ":locals" => {
                    let header = session.header();
                    let _ = writeln!(stdout, "{}", header.locals_summary);
                }
                ":d" | ":diag" => {
                    let header = session.header();
                    let _ = writeln!(stdout, "{}", header.last_diag_summary);
                }
                ":h" | ":header" => {
                    let header = session.header();
                    let _ = writeln!(stdout, "{}", header.inputs_summary);
                    let _ = writeln!(stdout, "{}", header.locals_summary);
                    let _ = writeln!(stdout, "{}", header.last_diag_summary);
                }
                _ => {
                    let _ = writeln!(stdout, "unknown command: {trimmed}");
                }
            }
            let _ = stdout.flush();
            continue;
        }

        // Normal input line
        let _ = rl.add_history_entry(trimmed);
        let out = session.process_line(trimmed);
        for msg in out.messages {
            let _ = writeln!(stdout, "{msg}");
        }
        let _ = stdout.flush();
    }
}

fn repl_command(repl: Repl) {
    let loaded = load_effective_config_from_cwd();
    let mut repl_cfg: ReplConfig = loaded.repl;
    if repl.no_opt {
        repl_cfg.opt_enabled = false;
    }
    let mut session = ReplSession::new(repl_cfg);
    session.set_diag_config(loaded.diag);
    session.set_opt_config(loaded.opt);
    repl_line_mode(&mut session);
}

fn main() {
    match cli_parser().run() {
        Command::Run(run) => run_command(run),
        Command::Repl(repl) => repl_command(repl),
    }
}
