use std::fs;
use std::path::PathBuf;

use clap::CommandFactory;
use clap::Parser;
use miette::Context;
use miette::IntoDiagnostic;
use tracing_subscriber::EnvFilter;

mod config;
mod error;
mod render;
mod resolve;

#[derive(Parser)]
#[command(about = "Resolve environment variables from envoke.yaml", version)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
    /// Target environment (e.g. local, prod). Not required with --schema or
    /// --list-* flags.
    #[arg(
        env = "ENVOKE_ENV",
        required_unless_present_any = ["schema", "completions", "list_environments", "list_overrides", "list_tags", "list_everything"]
    )]
    environment: Option<String>,

    /// Write output to a file instead of stdout.
    #[arg(short, long)]
    output: Option<PathBuf>,

    /// Only include tagged variables with a matching tag. Repeatable.
    /// Untagged variables are always included.
    #[arg(short = 't', long = "tag")]
    tags: Vec<String>,

    /// Include all tagged variables regardless of their tags.
    #[arg(long, conflicts_with = "tags")]
    all_tags: bool,

    /// Select named overrides for source selection. Repeatable.
    /// Per variable, at most one active override may be defined.
    #[arg(short = 'O', long = "override")]
    overrides: Vec<String>,

    /// Prefix each line with `export`. Ignored when `--template` is used.
    #[arg(long)]
    prepend_export: bool,

    /// Path to config file.
    #[arg(short, long, default_value = "envoke.yaml")]
    config: PathBuf,

    /// Use a custom output template file instead of the built-in format.
    #[arg(
        long,
        long_help = "\
Use a custom output template file instead of the built-in format.
The template uses Jinja2 syntax (minijinja).

Template context:

  variables  Map of name -> {value, description}. Iterate with:
               {% for name, var in variables | items %}
             Access fields: {{ variables.DB_URL.value }}

  v          Flat map of name -> value string. Shorthand:
               {{ v.DB_URL }}

  meta       Invocation metadata:
               meta.timestamp        RFC 3339 timestamp
               meta.invocation       Full CLI invocation string
               meta.invocation_args  CLI args as a list
               meta.environment      Target environment name
               meta.config_file      Path to the config file
               meta.tags             Active --tag values as a list
               meta.overrides        Active --override values as a list

Available filters:

  Built-in (minijinja builtins):
    upper, lower, replace, trim, default, join, sort, length,
    first, last, reverse, title, capitalize, list, int, float,
    abs, round, batch, slice, indent, truncate, unique, map,
    select, reject, selectattr, rejectattr, tojson, and more.
    See https://docs.rs/minijinja/latest/minijinja/filters

  Additional filters:
    shell_escape  Escapes single quotes for shell safety
    urlencode     Percent-encodes special characters

All filters are available in both variable templates (the `template`
source type) and custom output templates.

Note: Variable template sources (the `template` source type in
envoke.yaml) also have access to a `meta` object:
  meta.environment      Target environment name
  meta.tags             Active --tag values as a list
  meta.overrides        Active --override values as a list
  meta.timestamp        RFC 3339 timestamp"
    )]
    template: Option<PathBuf>,

    /// Print the JSON Schema for envoke.yaml and exit.
    #[arg(long)]
    schema: bool,

    /// List all environment names found in the config and exit.
    #[arg(long, group = "list")]
    list_environments: bool,

    /// List all override names found in the config and exit.
    #[arg(long, group = "list")]
    list_overrides: bool,

    /// List all tag names found in the config and exit.
    #[arg(long, group = "list")]
    list_tags: bool,

    /// List all environments, overrides, and tags found in the config and exit.
    /// Each line is prefixed with the type (environment, override, tag).
    #[arg(long, group = "list")]
    list_everything: bool,

    /// Generate shell completions for the given shell and exit.
    #[arg(long)]
    completions: Option<clap_complete::Shell>,

    /// Disable parallel resolution of command and shell sources.
    #[arg(long)]
    no_parallel: bool,

    /// Suppress informational messages on stderr.
    #[arg(short, long)]
    quiet: bool,
}

fn run() -> miette::Result<()> {
    tracing_subscriber::fmt()
        .with_writer(std::io::stderr)
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    let cli = Cli::parse();

    if cli.schema {
        let schema = schemars::schema_for!(config::Config);
        let json = serde_json::to_string_pretty(&schema)
            .into_diagnostic()
            .context("failed to serialize schema")?;
        if let Some(path) = &cli.output {
            fs::write(path, &json)
                .into_diagnostic()
                .with_context(|| format!("failed to write {}", path.display()))?;
        } else {
            println!("{json}");
        }
        return Ok(());
    }

    if let Some(shell) = cli.completions {
        clap_complete::generate(shell, &mut Cli::command(), "envoke", &mut std::io::stdout());
        return Ok(());
    }

    if cli.list_environments || cli.list_overrides || cli.list_tags || cli.list_everything {
        let yaml = fs::read_to_string(&cli.config)
            .into_diagnostic()
            .with_context(|| format!("failed to read {}", cli.config.display()))?;
        let config: config::Config = serde_yml::from_str(&yaml)
            .into_diagnostic()
            .with_context(|| format!("failed to parse {}", cli.config.display()))?;

        if cli.list_environments {
            for name in config.environments() {
                println!("{name}");
            }
        } else if cli.list_overrides {
            for name in config.override_names() {
                println!("{name}");
            }
        } else if cli.list_tags {
            for name in config.tag_names() {
                println!("{name}");
            }
        } else if cli.list_everything {
            for name in config.environments() {
                println!("environment:{name}");
            }
            for name in config.override_names() {
                println!("override:{name}");
            }
            for name in config.tag_names() {
                println!("tag:{name}");
            }
        }
        return Ok(());
    }

    // Default: generate
    {
        let tags = cli.tags;
        let all_tags = cli.all_tags;
        let overrides = cli.overrides;
        let environment = cli.environment.expect("required by clap");
        let output = cli.output;
        let prepend_export = cli.prepend_export;
        let template_path = cli.template;
        let quiet = cli.quiet;

        let yaml = fs::read_to_string(&cli.config)
            .into_diagnostic()
            .with_context(|| format!("failed to read {}", cli.config.display()))?;
        let config: config::Config = serde_yml::from_str(&yaml)
            .into_diagnostic()
            .with_context(|| format!("failed to parse {}", cli.config.display()))?;

        let tags = if all_tags { config.tag_names() } else { tags };

        if !quiet {
            eprintln!("Generating environment variables for {environment}...");
        }

        let timestamp = chrono::Local::now().to_rfc3339();

        let parallel = !cli.no_parallel;
        let resolved = resolve::resolve_all(
            &config,
            &environment,
            &tags,
            &overrides,
            &timestamp,
            parallel,
        )
        .map_err(|errors| error::ResolveErrors { errors })?;

        let invocation_args: Vec<String> = std::env::args().collect();
        let ctx = render::RenderContext {
            resolved,
            meta: render::Meta {
                timestamp,
                invocation: invocation_args.join(" "),
                invocation_args,
                environment,
                config_file: cli.config.display().to_string(),
                tags,
                overrides,
            },
        };

        let content = if let Some(path) = &template_path {
            render::render_custom(&ctx, path)?
        } else if prepend_export {
            render::render_default_export(&ctx)?
        } else {
            render::render_default(&ctx)?
        };

        if let Some(path) = &output {
            fs::write(path, &content)
                .into_diagnostic()
                .with_context(|| format!("failed to write {}", path.display()))?;
            if !quiet {
                eprintln!("Wrote to {}", path.display());
            }
        } else {
            print!("{content}");
        }

        Ok(())
    }
}

fn main() -> miette::Result<()> {
    miette::set_hook(Box::new(|_| {
        Box::new(
            miette::MietteHandlerOpts::new()
                .terminal_links(true)
                .unicode(true)
                .context_lines(2)
                .build(),
        )
    }))
    .expect("miette hook should only be set once");
    run()
}
