use aiken_lang::ast::{TraceLevel, Tracing};
use aiken_project::watch::{self, watch_project, with_project};
use clap::builder::{MapValueParser, PossibleValuesParser, TypedValueParser};
use std::{path::PathBuf, process};

#[derive(clap::Args)]
/// Build an Aiken project
pub struct Args {
    /// Path to project
    directory: Option<PathBuf>,

    /// Deny warnings; warnings will be treated as errors
    #[clap(short = 'D', long)]
    deny: bool,

    /// Silence warnings; warnings will not be printed
    #[clap(short = 'S', long)]
    silent: bool,

    /// When enabled, re-run the command on file changes instead of exiting
    #[clap(short, long)]
    watch: bool,

    /// Also dump textual uplc
    #[clap(short, long)]
    uplc: bool,

    /// Environment to build against.
    #[clap(long)]
    env: Option<String>,

    /// Optional relative filepath to the generated Plutus blueprint.
    ///
    /// [default: plutus.json]
    #[clap(
        short,
        long("out"),
        value_parser,
        value_name = "FILEPATH",
        verbatim_doc_comment
    )]
    output: Option<PathBuf>,

    /// Filter traces to be included in the generated program(s).
    ///
    ///   - user-defined:
    ///       only consider traces that you've explicitly defined
    ///       either through the 'trace' keyword of via the trace-if-false
    ///       ('?') operator.
    ///
    ///   - compiler-generated:
    ///       only included internal traces generated by the
    ///       Aiken compiler, for example in usage of 'expect'.
    ///
    ///   - all:
    ///       include both user-defined and compiler-generated traces.
    ///
    /// [optional] [default: all]
    #[clap(short = 'f', long, value_parser=trace_filter_parser(), default_missing_value="all", verbatim_doc_comment, alias="filter_traces")]
    trace_filter: Option<fn(TraceLevel) -> Tracing>,

    /// Choose the verbosity level of traces:
    ///
    ///   - silent:
    ///       disable traces altogether
    ///
    ///   - compact:
    ///       only culprit line numbers are shown on failures
    ///
    ///   - verbose:
    ///       enable full verbose traces as provided by the user or the compiler
    ///
    /// [optional]
    #[clap(short, long, value_parser=trace_level_parser(), default_value_t=TraceLevel::Silent, verbatim_doc_comment)]
    trace_level: TraceLevel,
}

pub fn exec(
    Args {
        directory,
        deny,
        silent,
        watch,
        uplc,
        trace_filter,
        trace_level,
        output,
        env,
    }: Args,
) -> miette::Result<()> {
    let result = if watch {
        watch_project(directory.as_deref(), watch::default_filter, 500, |p| {
            p.build(
                uplc,
                match trace_filter {
                    Some(trace_filter) => trace_filter(trace_level),
                    None => Tracing::All(trace_level),
                },
                p.blueprint_path(output.as_deref()),
                env.clone(),
            )
        })
    } else {
        with_project(directory.as_deref(), deny, silent, false, |p| {
            p.build(
                uplc,
                match trace_filter {
                    Some(trace_filter) => trace_filter(trace_level),
                    None => Tracing::All(trace_level),
                },
                p.blueprint_path(output.as_deref()),
                env.clone(),
            )
        })
    };

    result.map_err(|_| process::exit(1))
}

#[allow(clippy::type_complexity)]
pub fn trace_filter_parser(
) -> MapValueParser<PossibleValuesParser, fn(String) -> fn(TraceLevel) -> Tracing> {
    PossibleValuesParser::new(["user-defined", "compiler-generated", "all"]).map(
        |s: String| match s.as_str() {
            "user-defined" => Tracing::UserDefined,
            "compiler-generated" => Tracing::CompilerGenerated,
            "all" => Tracing::All,
            _ => unreachable!(),
        },
    )
}

#[allow(clippy::type_complexity)]
pub fn trace_level_parser() -> MapValueParser<PossibleValuesParser, fn(String) -> TraceLevel> {
    PossibleValuesParser::new(["silent", "compact", "verbose"]).map(|s| match s.as_str() {
        "silent" => TraceLevel::Silent,
        "compact" => TraceLevel::Compact,
        "verbose" => TraceLevel::Verbose,
        _ => unreachable!(),
    })
}
