mod analyzer;
mod editor;
mod model;
mod parser;
mod pdf;
mod template;

use clap::{Parser, Subcommand};
use colored::Colorize;
use std::fs;
use std::path::{Path, PathBuf};

// ── CLI definition ────────────────────────────────────────────
// #[derive(Parser)] tells clap to generate CLI parsing from this struct
#[derive(Parser)]
#[command(
    name    = "txt2tex",
    about   = "Plain text resume → professional PDF",
    version = "0.1.0",
    long_about = "
txt2tex converts a plain .txt resume file into a professional PDF.

USAGE:
  txt2tex                open resume.txt in editor, generate PDF
  txt2tex new            create fresh resume.txt from template
  txt2tex analyze        check resume for weak spots
  txt2tex --no-edit      skip editor, just generate PDF
  txt2tex --dry-run      parse and print data, no PDF generated
    "
)]
struct Cli {
    // Option<Cmd> means the subcommand is optional
    // if the user runs just txt2tex with no subcommand, this is None
    #[command(subcommand)]
    command: Option<Cmd>,

    // --no-edit flag: skip opening the editor
    // useful when you have already edited the file manually
    /// Skip opening the editor
    #[arg(long)]
    no_edit: bool,

    // --dry-run flag: parse and show data, do not generate PDF
    // useful for debugging your resume.txt format
    /// Show parsed data without generating PDF
    #[arg(long)]
    dry_run: bool,
}

// the available subcommands
#[derive(Subcommand)]
enum Cmd {
    /// Create a fresh resume.txt from template in the current folder
    New,
    /// Analyze resume.txt and give feedback on weak spots
    Analyze,
}

// ── entry point ───────────────────────────────────────────────
fn main() {
    // Cli::parse() reads command line arguments and fills the struct
    // if arguments are invalid, clap prints an error and exits
    let cli = Cli::parse();

    // run() does the real work and returns Result
    // if there is an error, print it in red and exit with code 1
    if let Err(e) = run(cli) {
        eprintln!("\n{} {}\n", "Error:".red().bold(), e);
        std::process::exit(1);
    }
}

// ── main logic ────────────────────────────────────────────────
fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
    println!("{}", "\ntxt2tex — plain text to professional PDF\n".cyan().bold());

    // match on the subcommand
    // Some(Cmd::New)     → user ran: txt2tex new
    // Some(Cmd::Analyze) → user ran: txt2tex analyze
    // None               → user ran: txt2tex
    match cli.command {

        // ── txt2tex new ───────────────────────────────────────
        // creates a fresh resume.txt in the current folder
        Some(Cmd::New) => {
            let path = PathBuf::from("resume.txt");

            // do not overwrite if it already exists
            if path.exists() {
                return Err(
                    "resume.txt already exists here.\n\
                     Delete it first, or run txt2tex to edit the existing one.".into()
                );
            }

            // include_str! embeds the file content AT COMPILE TIME
            // the templates/jake.txt content is baked into the binary
            // so the templates folder does not need to exist at runtime
            let template = include_str!("../templates/jake.txt");
            fs::write(&path, template)?;

            println!("{}", "Created resume.txt in current folder.".green());
            println!("{}", "Opening editor...\n".cyan());

            // open editor for the user to fill in their details
            editor::open(&path)?;

            // generate PDF after editor closes
            generate_pdf(&path, cli.dry_run)?;
        }

        // ── txt2tex analyze ───────────────────────────────────
        // checks resume.txt for weak spots
        Some(Cmd::Analyze) => {
            let path = require_resume()?;
            let resume = parser::parse_file(path_str(&path)?)?;
            analyzer::analyze(&resume);
        }

        // ── txt2tex (no subcommand) ───────────────────────────
        // the default: edit resume.txt and generate PDF
        None => {
            // get resume.txt from current folder
            // if it does not exist, create it from template first
            let path = get_or_create_resume()?;

            if !cli.no_edit {
                editor::open(&path)?;
            }

            generate_pdf(&path, cli.dry_run)?;
        }
    }

    Ok(())
}

// ── helper functions ──────────────────────────────────────────

// require resume.txt to exist in the current folder
// returns an error with a helpful message if it does not
fn require_resume() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let path = PathBuf::from("resume.txt");
    if !path.exists() {
        return Err(
            "No resume.txt found in this folder.\nRun: txt2tex new".into()
        );
    }
    Ok(path)
}

// get resume.txt from current folder
// if it does not exist, create it from template and tell the user
fn get_or_create_resume() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let path = PathBuf::from("resume.txt");

    if !path.exists() {
        let template = include_str!("../templates/jake.txt");
        fs::write(&path, template)?;
        println!("{}", "Created resume.txt in current folder.".green());
        println!("{}", "Fill in your details, save and quit to generate PDF.\n".cyan());
    }

    Ok(path)
}

// convert PathBuf to &str, fail if path has invalid characters
fn path_str(path: &Path) -> Result<&str, Box<dyn std::error::Error>> {
    path.to_str().ok_or("File path contains invalid characters".into())
}

// parse resume.txt → render LaTeX → compile PDF
fn generate_pdf(path: &Path, dry_run: bool) -> Result<(), Box<dyn std::error::Error>> {
    println!("{}", "Parsing resume...".cyan());

    let resume = parser::parse_file(path_str(path)?)?;

    // validate minimum required fields
    if !resume.is_valid() {
        return Err(
            "Resume needs at least a name and email.\n\
             Edit resume.txt and fill them in.".into()
        );
    }

    // --dry-run: just print the parsed data and stop
    // useful for debugging your resume.txt format
    if dry_run {
        println!("\n{}", "Parsed data:".yellow().bold());
        // {:#?} is the pretty-print debug format
        // requires #[derive(Debug)] on the struct
        println!("{:#?}", resume);
        println!("\n{}", "Dry run complete. No PDF generated.".yellow());
        return Ok(());
    }

    println!("{}", "Rendering LaTeX...".cyan());
    let latex = template::render(&resume)?;

    // save PDF in the current working directory
    // same folder where the user ran txt2tex from
    let cwd = std::env::current_dir()?;

    println!("{}", "Compiling PDF with tectonic...".cyan());
    let pdf_path = pdf::compile(&latex, &cwd)?;

    println!(
        "\n{}\n  {}\n",
        "Done! PDF saved to:".green().bold(),
        pdf_path.display()
    );

    // open the PDF automatically with the system viewer
    open_pdf(&pdf_path);

    Ok(())
}

// open PDF with the system's default viewer
// cfg! makes each block only compile on the matching OS
fn open_pdf(path: &PathBuf) {
    #[cfg(target_os = "macos")]
    let _ = std::process::Command::new("open").arg(path).spawn();

    #[cfg(target_os = "linux")]
    let _ = std::process::Command::new("xdg-open").arg(path).spawn();

    #[cfg(target_os = "windows")]
    let _ = std::process::Command::new("cmd")
        .args(["/C", "start", "", &path.to_string_lossy()])
        .spawn();
}
