use crate::config::toml::ComponentLocationToml;
use clap::Parser;
use concepts::{
    ComponentType, ExecutionId, FunctionFqn, FunctionFqnParseError,
    prefixed_ulid::{DeploymentId, ExecutionIdDerived},
};
use std::{path::PathBuf, str::FromStr};

/// A deployment source: either a path to a TOML file or an existing deployment ID.
#[derive(Debug, Clone)]
pub(crate) enum DeploymentSource {
    File(PathBuf),
    Id(DeploymentId),
}

impl FromStr for DeploymentSource {
    type Err = std::convert::Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if let Ok(id) = s.parse::<DeploymentId>() {
            Ok(DeploymentSource::Id(id))
        } else {
            Ok(DeploymentSource::File(PathBuf::from(s)))
        }
    }
}

pub(crate) mod shadow {
    pub(crate) const PKG_VERSION: &str = env!("PKG_VERSION");
}

#[derive(Parser, Debug)]
#[clap(name = "obelisk")]
#[command
(
    version = const_format::formatcp!("{}", shadow::PKG_VERSION),
about = "Obelisk: deterministic workflow engine", disable_version_flag = true, disable_help_subcommand = true)]
pub(crate) struct Args {
    #[command(subcommand)]
    pub(crate) command: Subcommand,

    /// Print version
    #[arg(short, long, action = clap::ArgAction::Version)]
    version: Option<bool>,
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Subcommand {
    #[command(subcommand)]
    Server(Server),
    #[command(subcommand)]
    Execution(Execution),
    #[command(subcommand)]
    Component(Component),
    #[command(subcommand)]
    Deployment(Deployment),
    #[command(subcommand)]
    Generate(Generate),
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Deployment {
    /// Upload a deployment TOML as a new deployment; print the new deployment ID.
    Submit {
        /// Path to the deployment TOML file.
        #[arg(
            value_name = "PATH",
            required_unless_present = "empty",
            conflicts_with = "empty"
        )]
        file: Option<PathBuf>,
        /// Submit an empty deployment with no components.
        #[arg(long)]
        empty: bool,
        /// Verify all environment variables before persisting.
        #[arg(long)]
        verify: bool,
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
    },
    /// Submit (if a file is given) and enqueue for next server restart.
    Enqueue {
        /// Path to a deployment TOML file, or an existing deployment ID.
        #[arg(
            value_name = "PATH|ID",
            required_unless_present = "empty",
            conflicts_with = "empty"
        )]
        source: Option<DeploymentSource>,
        /// Enqueue an empty deployment with no components.
        #[arg(long)]
        empty: bool,
        /// Verify all environment variables before enqueuing.
        #[arg(long)]
        verify: bool,
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
    },
    /// Submit (if a file is given) and hot-redeploy immediately. Fails if not possible.
    Apply {
        /// Path to a deployment TOML file, or an existing deployment ID.
        #[arg(
            value_name = "PATH|ID",
            required_unless_present = "empty",
            conflicts_with = "empty"
        )]
        source: Option<DeploymentSource>,
        /// Apply an empty deployment with no components.
        #[arg(long)]
        empty: bool,
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
    },
    /// List recent deployments.
    List {
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
    },
    /// Show the full configuration of a deployment.
    Show {
        /// Deployment ID
        #[arg(value_name = "ID")]
        id: String,
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
    },
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Generate {
    /// Generate the server configuration (server.toml) schema in JSON schema format.
    #[cfg(debug_assertions)]
    ServerConfigSchema {
        /// Filename to write the schema to, defaults to <stdout>.
        output: Option<PathBuf>,
    },
    /// Generate the deployment configuration (deployment.toml) schema in JSON schema format.
    #[cfg(debug_assertions)]
    DeploymentSchema {
        /// Filename to write the schema to, defaults to <stdout>.
        output: Option<PathBuf>,
    },
    /// Generate the canonical deployment schema (stored in the database) in JSON schema format.
    #[cfg(debug_assertions)]
    DeploymentCanonicalSchema {
        /// Filename to write the schema to, defaults to <stdout>.
        output: Option<PathBuf>,
    },
    /// Generate the database storage schema in JSON schema format.
    #[cfg(debug_assertions)]
    DbSchema {
        /// Filename to write the schema to, defaults to <stdout>.
        output: Option<PathBuf>,
    },
    /// Generate the `OpenAPI` schema in JSON format.
    #[cfg(debug_assertions)]
    OpenApiSchema {
        /// Filename to write the schema to, defaults to <stdout>.
        output: Option<PathBuf>,
    },
    /// Generate extension WIT files that are automatically implemented by Obelisk
    /// based on the exported interfaces of the component.
    WitExtensions {
        #[arg(long, short)]
        force: bool,
        /// One of `workflow`, `activity`, `activity_stub`, `webhook_endpoint`
        component_type: ComponentType,
        /// Path to the `wit` folder, containing the target world and possibly `deps` subfolder.
        input_wit_directory: PathBuf,
        /// Directory where folders and WIT files will be written to.
        output_directory: PathBuf,
    },
    /// Generate Obelisk WIT files for given component type.
    WitSupport {
        /// One of `workflow`, `activity`, `activity_stub`, `webhook_endpoint`
        component_type: ComponentType,
        /// Directory where folders and WIT files will be written to.
        output_directory: PathBuf,
        #[arg(long, short)]
        overwrite: bool,
    },
    /// Generate WIT dependency folder based on activities and workflows found in the deployment TOML.
    WitDeps {
        /// Path to the deployment TOML file.
        #[arg(long, short)]
        deployment: PathBuf,
        /// Directory where folders and WIT files will be written to.
        output_directory: PathBuf,
        /// Overwrite existing files.
        #[arg(long, short)]
        overwrite: bool,
    },
    /// Generate a default server.toml.
    ServerConfig {
        /// Filename to write the TOML to, defaults to `server.toml`.
        output: Option<PathBuf>,
        /// Overwrite existing file.
        #[arg(long, short)]
        overwrite: bool,
    },
    /// Generate a default deployment.toml.
    Deployment {
        /// Filename to write the TOML to, defaults to `deployment.toml`.
        output: Option<PathBuf>,
        /// Overwrite existing file.
        #[arg(long, short)]
        overwrite: bool,
    },
    ExecutionId,
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Server {
    Run {
        /// Clean the sqlite database directory
        #[arg(long)]
        clean_sqlite_directory: bool,
        /// Clean the codegen and OCI cache directories
        #[arg(long)]
        clean_cache: bool,
        /// Clean the codegen cache directory
        #[arg(long)]
        clean_codegen_cache: bool,
        /// Path to the server configuration file (server.toml). If omitted, built-in defaults are used.
        #[arg(long)]
        server_config: Option<PathBuf>,
        /// Path to the deployment TOML file. If provided, the deployment is inserted and activated on startup,
        /// overriding any existing Enqueued or Active deployment in the database.
        #[arg(long, conflicts_with = "deployment_empty")]
        deployment: Option<PathBuf>,
        /// Start with an empty deployment, ignoring any Enqueued or Active deployment in the database.
        /// Useful for recovering from a faulty deployment: start empty, then push a new deployment via gRPC.
        #[arg(long)]
        deployment_empty: bool,
        /// Ignore type checking errors
        #[arg(long, short)]
        suppress_type_checking_errors: bool,
    },
    /// Read the configuration, compile the components, verify their imports and exit
    Verify {
        /// Clean the codegen and OCI cache directories
        #[arg(long)]
        clean_cache: bool,
        /// Clean the codegen cache
        #[arg(long)]
        clean_codegen_cache: bool,
        /// Path to the server configuration file (server.toml). If omitted, built-in defaults are used.
        #[arg(long)]
        server_config: Option<PathBuf>,
        /// Path to the deployment TOML file. If omitted, the database's Enqueued deployment is used,
        /// falling back to the Active deployment. Errors if neither is found.
        #[arg(long)]
        deployment: Option<PathBuf>,
        /// Do not verify existence of environment variables
        #[arg(long, short)]
        ignore_missing_env_vars: bool,
        /// Ignore type checking errors
        #[arg(long, short)]
        suppress_type_checking_errors: bool,
        /// Do not check database schema
        #[arg(long)]
        skip_db: bool,
    },
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Component {
    /// List components.
    List {
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
        /// Show component imports
        #[arg(short, long)]
        imports: bool,
        /// Show auto-generated export extensions
        #[arg(short, long)]
        extensions: bool,
    },
    /// Push a WASM file to an OCI registry.
    Push {
        /// WASM file to be pushed
        #[arg(required(true))]
        path: PathBuf,
        /// OCI reference. Example: docker.io/repo/image:tag
        #[arg(required(true))]
        image_name: oci_client::Reference,
    },
    /// Add a component to the deployment TOML configuration file.
    Add {
        /// One of `workflow`, `activity`, `activity_stub`, `webhook_endpoint`
        #[arg(required(true))]
        component_type: ComponentType,
        /// Path to the WASM file
        #[arg(required(true))]
        location: ComponentLocationToml,
        #[arg(long, short)]
        name: String,
        /// Path to the deployment TOML file.
        #[arg(long, short)]
        deployment: PathBuf,
        /// Store the component in local cache and record its `content_digest` for reproducible builds.
        #[arg(long)]
        locked: bool,
    },
}

#[derive(Debug, Clone)]
pub enum FunctionFqnOrShort {
    Ffqn(FunctionFqn),
    Short {
        ifc_name: String,
        function_name: String,
    }, // starts with `.../` prefix
}
impl FromStr for FunctionFqnOrShort {
    type Err = FunctionFqnParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        const PREFIX: &str = ".../";

        if let Some(rest) = input.strip_prefix(PREFIX) {
            let Some((ifc_name, fn_name)) = rest.split_once('.') else {
                return Err(FunctionFqnParseError::DelimiterNotFound(input.to_string()));
            };
            // Ensure exactly two parts
            if fn_name.contains('.') {
                return Err(FunctionFqnParseError::DelimiterFoundInFunctionName(
                    input.to_string(),
                ));
            }

            Ok(FunctionFqnOrShort::Short {
                ifc_name: ifc_name.to_string(),
                function_name: fn_name.to_string(),
            })
        } else {
            Ok(FunctionFqnOrShort::Ffqn(FunctionFqn::from_str(input)?))
        }
    }
}

#[derive(Debug, clap::Subcommand)]
pub(crate) enum Execution {
    /// Submit new execution and optionally follow its status stream until the it finishes.
    Submit {
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
        #[arg(short, long)]
        execution_id: Option<ExecutionId>,
        /// Function in the fully qualified format or shortened to .../ifc.fn
        #[arg(value_name = "function")]
        ffqn: FunctionFqnOrShort,
        /// Follow the stream of events until the execution finishes
        #[arg(short, long)]
        follow: bool,
        /// Do not attempt to reconnect on connection error while following the status stream.
        #[arg(long, requires = "follow")]
        no_reconnect: bool,
        /// Output JSON in Web API format.
        #[arg(short, long)]
        json: bool,
        /// Accepted Parameter Formats:
        ///
        /// - JSON array string, e.g. '["first", "second", null, 1]'
        ///
        /// - File reference prefixed with @, e.g. @file.json (file must contain a valid JSON array)
        ///
        /// - Multiple arguments after --, e.g. -- '"first"' @secondparam.json null 1
        ///
        /// - For functions with no parameters: [] (JSON array variant) or no arguments.
        #[arg(name = "parameters")]
        params: Vec<String>,
    },
    /// Write a return value or an execution error to an already created stubbed execution.
    Stub(Stub),
    /// Get the current state of an execution.
    Get {
        /// Address of the obelisk server
        #[arg(short, long, default_value = "http://127.0.0.1:5005")]
        api_url: String,
        /// Follow the status stream until the execution finishes.
        #[arg(short, long)]
        follow: bool,
        execution_id: ExecutionId,
        /// Do not attempt to reconnect on connection error while following the status stream.
        #[arg(long, requires = "follow")]
        no_reconnect: bool,
    },
    Cancel(CancelCommand),
}

pub(crate) mod params {
    use clap::error::ErrorKind;
    use serde_json::Value;

    pub(crate) fn parse_params(params: Vec<String>) -> Result<Vec<serde_json::Value>, clap::Error> {
        if params.is_empty() {
            Ok(vec![]) // no params, does not matter if `--` was present.
        } else if params.len() == 1 && !dashdash() {
            let mut params = params;
            let json_array = params.pop().expect("checked that len == 1");
            // Single JSON Array, or a `@`-prefixed file containing the array.
            let json_array = if let Some(file_path) = json_array.strip_prefix('@') {
                std::fs::read_to_string(file_path).map_err(|err| {
                    clap::Error::raw(
                        ErrorKind::Io,
                        format!(
                            "parameter parsing failed: failed to read file '{file_path}': {err}"
                        ),
                    )
                })?
            } else {
                json_array
            };
            let json_value = serde_json::from_str(&json_array).map_err(|err| {
                clap::Error::raw(
                    ErrorKind::ValueValidation,
                    format!("Invalid JSON array for parameters: {err}"),
                )
            })?;
            let Value::Array(params) = json_value else {
                return Err(clap::Error::raw(
                    ErrorKind::ValueValidation,
                    "Parameter provided as JSON must be a JSON array.",
                ));
            };
            Ok(params)
        } else {
            // Fallback to raw arguments. Each argument is interpreted as a JSON value or a file starting with `@` that contains the JSON.
            let mut parsed_params: Vec<Value> = Vec::new();
            for (idx, arg) in params.into_iter().enumerate() {
                let arg = if let Some(file_path) = arg.strip_prefix('@') {
                    std::fs::read_to_string(file_path).map_err(|err| {
                        clap::Error::raw(
                            ErrorKind::Io,
                            format!(
                                "{}-th parameter parsing failed: failed to read file '{file_path}': {err}",
                                idx + 1
                            ),
                        )
                    })?
                } else {
                    arg
                };
                let json = serde_json::from_str(&arg).map_err(|err| {
                    clap::Error::raw(
                        ErrorKind::ValueValidation,
                        format!(
                            "cannot parse {}-th parameter `{arg}` as JSON -  {err}",
                            idx + 1
                        ),
                    )
                })?;
                parsed_params.push(json);
            }
            Ok(parsed_params)
        }
    }

    fn dashdash() -> bool {
        // Ambigous: Either the single JSON Array representing all parameters,
        // OR `-- "first-and-only-json-param"`
        let mut rev_arg_iter = std::env::args().rev();
        rev_arg_iter.next().expect("last arg must be present");
        let maybe_separator = rev_arg_iter.next().expect("last-1 arg must be present");
        maybe_separator == "--"
    }
}

#[derive(Debug, clap::Args)]
#[command()]
pub(crate) struct Stub {
    /// Address of the obelisk server
    #[arg(short, long, default_value = "http://127.0.0.1:5005")]
    pub(crate) api_url: String,
    /// Execution ID of the stub execution waiting for its return value.
    #[arg(value_name = "EXECUTION_ID")]
    pub(crate) execution_id: ExecutionIdDerived,

    /// Stub a return value encoded as JSON
    #[arg(value_name = "RETURN_VAL")]
    pub(crate) return_value: String,
}

#[derive(Debug, clap::Args)]
#[command()]
pub(crate) struct CancelCommand {
    /// Address of the obelisk server
    #[arg(short, long, default_value = "http://127.0.0.1:5005")]
    pub(crate) api_url: String,
    #[arg(value_name = "ID")]
    pub(crate) id: String,
}
