use clap::Parser as ClapParser;
use colored::Colorize;
use miette::{IntoDiagnostic, Result, bail};
use nexus_lang::{
    InterpretWarning, Interpreter, Network, NexusError, Node, Parser, PortDirection, Scanner,
    Source,
};
use rustyline::{DefaultEditor, error::ReadlineError};
use std::{
    fs,
    path::{Path, PathBuf},
};
use tabled::{
    Table, Tabled,
    settings::{Color, Modify, Style, object::Rows},
};

/// Nexus command-line interpreter tool.
#[derive(ClapParser, Debug)]
#[command(author, version, about)]
#[command(group = clap::ArgGroup::new("input").required(false).multiple(false).args(&["source_code", "file"]))]
#[command(after_help = "Omit any source code input arguments to run the interactive REPL.")]
struct Args {
    /// Scan input source code from a string.
    #[arg(short, long, group = "input")]
    source_code: Option<String>,

    /// Scan input source code from an input file.
    #[arg(short, long, group = "input")]
    file: Option<PathBuf>,

    /// Additional library search paths for 'use' imports (repeatable).
    #[arg(short = 'I', long = "search-path", value_name = "DIR")]
    search_paths: Vec<PathBuf>,
}

fn main() -> Result<()> {
    let Args {
        source_code,
        file,
        search_paths,
    } = Args::parse();

    if let Some(source_code) = source_code {
        execute_code(source_code.as_str(), None, search_paths)?;
    } else if let Some(file) = file {
        execute_file(file, search_paths)?;
    } else {
        run_repl()?;
    }

    Ok(())
}

fn execute_code(
    source_code: &str,
    file_name: Option<&str>,
    search_paths: Vec<PathBuf>,
) -> Result<()> {
    let source = Source::new_with_name(source_code, file_name.unwrap_or("source"));

    print!("{}:\n{source}\n", "Source input".white().bold());
    if !source.code().ends_with('\n') {
        println!();
    }

    let tokens = Scanner::new(source).scan().map_err(NexusError::from)?;

    println!(
        "{}:\n[{}]\n",
        "Scanned tokens".white().bold(),
        tokens
            .iter()
            .map(|token| format!("{token}"))
            .collect::<Vec<_>>()
            .join(", ")
    );

    let ast = Parser::new(source, tokens)
        .parse()
        .map_err(NexusError::from)?;

    println!(
        "{}:\n[\n{}\n]\n",
        "Parsed AST".white().bold(),
        ast.iter()
            .map(|n| format!("  {n}"))
            .collect::<Vec<_>>()
            .join("\n"),
    );

    println!("{}:\n", "Interpreted result".white().bold());

    let (network, warnings) = Interpreter::new(source, ast)
        .with_search_paths(search_paths)
        .eval()
        .map_err(NexusError::from)?;

    println!();

    print_warnings(&warnings);

    print_network(&network);

    Ok(())
}

fn print_warnings(warnings: &[InterpretWarning]) {
    for w in warnings {
        eprintln!("{:?}", miette::Report::new(w.clone()));
    }
}

/// Format a port direction as a colored string.
fn format_direction(dir: PortDirection) -> String {
    match dir {
        PortDirection::In => "in".bright_blue().to_string(),
        PortDirection::Out => "out".bright_red().to_string(),
        PortDirection::Inout => "inout".bright_magenta().to_string(),
    }
}

/// Format a port list as a comma-separated string like `"in input, out output"`.
fn format_ports(ports: &[nexus_lang::Port]) -> String {
    if ports.is_empty() {
        return String::new();
    }
    ports
        .iter()
        .map(|p| format!("{} {}", format_direction(p.direction), p.name))
        .collect::<Vec<_>>()
        .join(", ")
}

/// Row type for the nodes table.
#[derive(Tabled)]
struct NodeRow {
    path: String,
    #[tabled(rename = "type")]
    schema_type: String,
    ports: String,
}

/// Row type for the groups table.
#[derive(Tabled)]
struct GroupRow {
    path: String,
    ports: String,
}

/// Row type for the connections table.
#[derive(Tabled)]
struct ConnectionRow {
    from: String,
    to: String,
}

/// Walk the node tree and collect rows for leaf nodes and group nodes separately.
fn collect_rows(
    node: &Node,
    path: &str,
    leaf_rows: &mut Vec<NodeRow>,
    group_rows: &mut Vec<GroupRow>,
) {
    if node.children.is_empty() {
        leaf_rows.push(NodeRow {
            path: path.to_owned(),
            schema_type: node.type_name.clone(),
            ports: format_ports(&node.ports),
        });
    } else {
        group_rows.push(GroupRow {
            path: path.to_owned(),
            ports: format_ports(&node.ports),
        });
        for child in &node.children {
            collect_rows(
                child,
                &format!("{path}/{}", child.name),
                leaf_rows,
                group_rows,
            );
        }
    }
}

/// Print the network as tabled tables.
fn print_network(network: &Network) {
    let paths = network.node_paths();

    let mut leaf_rows: Vec<NodeRow> = Vec::new();
    let mut group_rows: Vec<GroupRow> = Vec::new();

    if let Some(root) = &network.root {
        collect_rows(root, &root.name, &mut leaf_rows, &mut group_rows);
    }

    println!("{}:", "Network".white().bold());
    println!();

    // Nodes table.
    println!("Nodes:");
    if leaf_rows.is_empty() {
        println!("(none)");
    } else {
        println!(
            "{}",
            Table::new(leaf_rows)
                .with(Style::modern())
                .with(Modify::new(Rows::first()).with(Color::BOLD))
        );
    }
    println!();

    // Groups table.
    println!("Groups:");
    if group_rows.is_empty() {
        println!("(none)");
    } else {
        println!(
            "{}",
            Table::new(group_rows)
                .with(Style::modern())
                .with(Modify::new(Rows::first()).with(Color::BOLD))
        );
    }
    println!();

    // Connections table.
    println!("Connections:");
    if network.connections.is_empty() {
        println!("(none)");
    } else {
        let conn_rows: Vec<ConnectionRow> = network
            .connections
            .iter()
            .map(|c| {
                let from_path = paths.get(&c.from.node_id).cloned().unwrap_or_default();
                let to_path = paths.get(&c.to.node_id).cloned().unwrap_or_default();
                ConnectionRow {
                    from: format!("{from_path}/{}", c.from.port_name),
                    to: format!("{to_path}/{}", c.to.port_name),
                }
            })
            .collect();
        println!(
            "{}",
            Table::new(conn_rows)
                .with(Style::modern())
                .with(Modify::new(Rows::first()).with(Color::BOLD))
        );
    }
    println!();
}

fn execute_file(file: impl AsRef<Path>, search_paths: Vec<PathBuf>) -> Result<()> {
    let source_code = fs::read_to_string(&file).map_err(NexusError::from)?;
    let file_name = file.as_ref().display().to_string();

    execute_code(source_code.as_str(), Some(file_name.as_str()), search_paths)
}

fn run_repl() -> Result<()> {
    let mut rl = DefaultEditor::new().into_diagnostic()?;

    println!("{}", "Interactive Nexus REPL. Press Ctrl+D to exit.".bold());

    loop {
        match rl.readline("> ") {
            Ok(line) => {
                rl.add_history_entry(line.clone()).into_diagnostic()?;
                match execute_code(line.as_str(), None, Vec::new()) {
                    Ok(()) => {}
                    Err(e) => eprintln!("{e:?}"),
                }
            }
            Err(ReadlineError::Eof) => break,
            Err(ReadlineError::Interrupted) => {
                eprintln!("interrupted");
                break;
            }
            _ => {
                bail!("failed to parse input");
            }
        }
    }

    Ok(())
}
