mod artifacts;
mod definition;
mod metadata;
mod normalize;
mod wrapper_template;

use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use crate::config::{ConnectConfig, ConnectProtocol, GenerateConfig, RegenerateConfig};
use crate::generator::artifacts::{build_artifacts, ArtifactBuildOptions, GeneratedArtifacts};
use crate::generator::definition::load_definition;
use crate::generator::metadata::{
    read_metadata, sha256_hex, write_metadata, ArtifactMetadata, ArtifactMode, ArtifactPaths,
    ConnectProfileMetadata, GenerationMetadata, GenerationOptions, NormalizedMetadata,
    SourceMetadata,
};
use crate::generator::normalize::{
    env_template_map, header_template_map, normalize_definition, sanitize_artifact_name,
    NormalizedDefinition,
};

pub fn run_generate(config: &GenerateConfig) -> Result<(), String> {
    let definition_path = absolute_path(&config.definition)?;
    let output_dir = absolute_path(&config.out)?;

    let parsed = load_definition(&definition_path, config.server.as_deref())?;
    let normalized = normalize_definition(&parsed, config.artifact_name.as_deref())?;

    fs::create_dir_all(&output_dir)
        .map_err(|err| format!("Failed to create {}: {err}", output_dir.display()))?;

    let metadata_path = output_dir.join("mcpway-artifact.json");
    let artifact_outputs = build_artifacts(
        &output_dir,
        &normalized,
        &metadata_path,
        &ArtifactBuildOptions {
            bundle_mcpway: config.bundle_mcpway,
            mcpway_binary: config.mcpway_binary.clone(),
            compile_wrapper: config.compile_wrapper,
        },
    )?;

    let definition_bytes = fs::read(&definition_path).map_err(|err| {
        format!(
            "Failed to read definition {}: {err}",
            definition_path.display()
        )
    })?;

    let metadata = build_metadata(MetadataBuildInput {
        normalized: &normalized,
        definition_path: &definition_path,
        server_selector: Some(normalized.server_name.clone()),
        definition_bytes: &definition_bytes,
        output_dir: &output_dir,
        options: GenerationOptions {
            bundle_mcpway: config.bundle_mcpway,
            compile_wrapper: config.compile_wrapper,
        },
        artifacts: &artifact_outputs,
    });

    write_metadata(&metadata_path, &metadata)?;

    println!("[mcpway] Generated artifact: {}", output_dir.display());
    println!("[mcpway] Regenerate with: {}", metadata.regenerate_command);

    Ok(())
}

pub fn save_connect_profile(
    config: &ConnectConfig,
    protocol: ConnectProtocol,
) -> Result<(), String> {
    let spec = connect_profile_from_config(config, protocol)?;
    let metadata = write_connect_profile(&spec)?;

    println!(
        "[mcpway] Saved connect profile: {}",
        spec.output_dir.display()
    );
    println!("[mcpway] Regenerate with: {}", metadata.regenerate_command);

    Ok(())
}

pub fn run_regenerate(config: &RegenerateConfig) -> Result<(), String> {
    let metadata_path = absolute_path(&config.metadata)?;
    let existing = read_metadata(&metadata_path)?;

    if existing.mode == ArtifactMode::Connect {
        let spec = connect_profile_from_metadata(&existing, config.out.as_deref())?;
        let metadata = write_connect_profile(&spec)?;
        println!(
            "[mcpway] Regenerated connect profile: {}",
            spec.output_dir.display()
        );
        println!("[mcpway] Regenerate with: {}", metadata.regenerate_command);
        return Ok(());
    }

    let definition_path = if let Some(path) = &config.definition {
        absolute_path(path)?
    } else {
        PathBuf::from(existing.source.definition_path.clone())
    };

    let out = if let Some(path) = &config.out {
        absolute_path(path)?
    } else {
        PathBuf::from(existing.output_dir.clone())
    };

    let bundle_mcpway = config
        .bundle_mcpway
        .unwrap_or(existing.generation.options.bundle_mcpway);
    let compile_wrapper = config
        .compile_wrapper
        .unwrap_or(existing.generation.options.compile_wrapper);

    let generate_config = GenerateConfig {
        definition: definition_path,
        server: config
            .server
            .clone()
            .or(existing.source.server_selector.clone()),
        out,
        artifact_name: Some(existing.artifact_name.clone()),
        bundle_mcpway,
        mcpway_binary: config.mcpway_binary.clone(),
        compile_wrapper,
    };

    run_generate(&generate_config)
}

struct MetadataBuildInput<'a> {
    normalized: &'a NormalizedDefinition,
    definition_path: &'a Path,
    server_selector: Option<String>,
    definition_bytes: &'a [u8],
    output_dir: &'a Path,
    options: GenerationOptions,
    artifacts: &'a GeneratedArtifacts,
}

fn build_metadata(input: MetadataBuildInput<'_>) -> ArtifactMetadata {
    let metadata_path = input.output_dir.join("mcpway-artifact.json");
    ArtifactMetadata {
        mode: ArtifactMode::Generate,
        schema_version: "1".to_string(),
        generated_at_utc: unix_timestamp_utc_string(),
        artifact_name: input.normalized.artifact_name.clone(),
        output_dir: input.output_dir.to_string_lossy().to_string(),
        source: SourceMetadata {
            definition_path: input.definition_path.to_string_lossy().to_string(),
            server_selector: input.server_selector,
            definition_sha256: sha256_hex(input.definition_bytes),
        },
        normalized: NormalizedMetadata {
            command: input.normalized.command.clone(),
            args: input.normalized.args.clone(),
            env_template: env_template_map(input.normalized),
            headers_template: header_template_map(input.normalized),
        },
        generation: GenerationMetadata {
            options: input.options,
        },
        artifacts: ArtifactPaths {
            script_path: input.artifacts.script_path.to_string_lossy().to_string(),
            wrapper_path: input
                .artifacts
                .wrapper_path
                .as_ref()
                .map(|path| path.to_string_lossy().to_string()),
            mcpway_path: input
                .artifacts
                .mcpway_path
                .as_ref()
                .map(|path| path.to_string_lossy().to_string()),
            env_example_path: input
                .artifacts
                .env_example_path
                .to_string_lossy()
                .to_string(),
        },
        regenerate_command: format!(
            "mcpway regenerate --metadata {}",
            shell_words::quote(&metadata_path.to_string_lossy())
        ),
        connect: None,
    }
}

#[derive(Debug, Clone)]
struct ConnectProfileSpec {
    artifact_name: String,
    endpoint: String,
    protocol: String,
    header_bindings: Vec<(String, String)>,
    output_dir: PathBuf,
}

fn connect_profile_from_config(
    config: &ConnectConfig,
    protocol: ConnectProtocol,
) -> Result<ConnectProfileSpec, String> {
    let endpoint = config
        .endpoint
        .as_deref()
        .ok_or_else(|| "connect profile endpoint is missing".to_string())?
        .to_string();
    let raw_output_dir = config
        .save_profile_dir
        .as_ref()
        .ok_or_else(|| "save profile path missing".to_string())?;
    let output_dir = absolute_path(raw_output_dir)?;

    let derived_name = derive_endpoint_profile_name(&endpoint);
    let artifact_name = config
        .profile_name
        .as_ref()
        .map(|name| sanitize_artifact_name(name))
        .filter(|name| !name.is_empty())
        .unwrap_or(derived_name);

    let mut header_bindings = Vec::new();
    let mut headers: Vec<_> = config.headers.keys().cloned().collect();
    headers.sort();
    for header in headers {
        header_bindings.push((header.clone(), header_env_var_name(&header)));
    }

    Ok(ConnectProfileSpec {
        artifact_name,
        endpoint,
        protocol: protocol.as_str().to_string(),
        header_bindings,
        output_dir,
    })
}

fn connect_profile_from_metadata(
    metadata: &ArtifactMetadata,
    out_override: Option<&Path>,
) -> Result<ConnectProfileSpec, String> {
    let connect = metadata
        .connect
        .as_ref()
        .ok_or("connect metadata missing for connect profile regeneration")?;

    let output_dir = if let Some(path) = out_override {
        absolute_path(path)?
    } else {
        PathBuf::from(metadata.output_dir.clone())
    };

    let artifact_name = sanitize_artifact_name(&metadata.artifact_name);
    if artifact_name.is_empty() {
        return Err("Invalid artifact name in metadata".to_string());
    }

    let mut header_bindings = Vec::new();
    for (header, template) in &connect.headers_template {
        let env_key =
            extract_template_env_key(template).unwrap_or_else(|| header_env_var_name(header));
        header_bindings.push((header.clone(), env_key));
    }
    header_bindings.sort_by(|a, b| a.0.cmp(&b.0));

    Ok(ConnectProfileSpec {
        artifact_name,
        endpoint: connect.endpoint.clone(),
        protocol: connect.protocol.clone(),
        header_bindings,
        output_dir,
    })
}

fn write_connect_profile(spec: &ConnectProfileSpec) -> Result<ArtifactMetadata, String> {
    let bin_dir = spec.output_dir.join("bin");
    fs::create_dir_all(&bin_dir)
        .map_err(|err| format!("Failed to create {}: {err}", bin_dir.display()))?;

    let script_path = write_connect_profile_script(&bin_dir, spec)?;
    let env_example_path = write_connect_env_example(&spec.output_dir, spec)?;
    let metadata_path = spec.output_dir.join("mcpway-artifact.json");

    let mut headers_template = BTreeMap::new();
    for (header, env_key) in &spec.header_bindings {
        headers_template.insert(header.clone(), format!("${{{env_key}}}"));
    }

    let metadata = ArtifactMetadata {
        mode: ArtifactMode::Connect,
        schema_version: "1".to_string(),
        generated_at_utc: unix_timestamp_utc_string(),
        artifact_name: spec.artifact_name.clone(),
        output_dir: spec.output_dir.to_string_lossy().to_string(),
        source: SourceMetadata {
            definition_path: "".to_string(),
            server_selector: None,
            definition_sha256: "".to_string(),
        },
        normalized: NormalizedMetadata {
            command: "".to_string(),
            args: Vec::new(),
            env_template: BTreeMap::new(),
            headers_template: headers_template.clone(),
        },
        generation: GenerationMetadata {
            options: GenerationOptions {
                bundle_mcpway: false,
                compile_wrapper: false,
            },
        },
        artifacts: ArtifactPaths {
            script_path: script_path.to_string_lossy().to_string(),
            wrapper_path: None,
            mcpway_path: None,
            env_example_path: env_example_path.to_string_lossy().to_string(),
        },
        regenerate_command: format!(
            "mcpway regenerate --metadata {}",
            shell_words::quote(&metadata_path.to_string_lossy())
        ),
        connect: Some(ConnectProfileMetadata {
            endpoint: spec.endpoint.clone(),
            protocol: spec.protocol.clone(),
            headers_template,
            profile_name: Some(spec.artifact_name.clone()),
        }),
    };

    write_metadata(&metadata_path, &metadata)?;
    Ok(metadata)
}

fn write_connect_profile_script(
    bin_dir: &Path,
    spec: &ConnectProfileSpec,
) -> Result<PathBuf, String> {
    let script_name = if cfg!(windows) {
        format!("{}.cmd", spec.artifact_name)
    } else {
        spec.artifact_name.clone()
    };
    let script_path = bin_dir.join(script_name);

    let script = if cfg!(windows) {
        render_windows_connect_profile_script(spec)
    } else {
        render_posix_connect_profile_script(spec)
    };

    fs::write(&script_path, script).map_err(|err| {
        format!(
            "Failed to write profile script {}: {err}",
            script_path.display()
        )
    })?;
    make_executable(&script_path)?;

    Ok(script_path)
}

fn render_posix_connect_profile_script(spec: &ConnectProfileSpec) -> String {
    let mut script = String::new();
    script.push_str("#!/usr/bin/env bash\n");
    script.push_str("set -euo pipefail\n\n");

    for (_header, env_key) in &spec.header_bindings {
        script.push_str(&format!(
            "if [[ -z \"${{{env_key}:-}}\" ]]; then echo \"Missing required environment variable: {env_key}\" >&2; exit 1; fi\n"
        ));
    }
    if !spec.header_bindings.is_empty() {
        script.push('\n');
    }

    script.push_str("ARGS=(connect");
    script.push_str(&format!(" {}", shell_single_quote(&spec.endpoint)));
    script.push_str(&format!(
        " --protocol {})\n",
        shell_single_quote(&spec.protocol)
    ));

    for (header, env_key) in &spec.header_bindings {
        script.push_str(&format!(
            "ARGS+=(--header \"{}: ${{{}}}\")\n",
            escape_for_double_quotes(header),
            env_key
        ));
    }

    script.push_str("exec mcpway \"${ARGS[@]}\" \"$@\"\n");
    script
}

fn render_windows_connect_profile_script(spec: &ConnectProfileSpec) -> String {
    let mut script = String::new();
    script.push_str("@echo off\r\n");
    script.push_str("setlocal enabledelayedexpansion\r\n");

    for (_header, env_key) in &spec.header_bindings {
        script.push_str(&format!(
            "if \"%{}%\"==\"\" ( echo Missing required environment variable: {} & exit /b 1 )\r\n",
            env_key, env_key
        ));
    }

    script.push_str(&format!(
        "mcpway connect \"{}\" --protocol \"{}\"",
        spec.endpoint.replace('"', "\\\""),
        spec.protocol.replace('"', "\\\"")
    ));

    for (header, env_key) in &spec.header_bindings {
        script.push_str(&format!(
            " --header \"{}: %{}%\"",
            header.replace('"', "\\\""),
            env_key
        ));
    }
    script.push_str(" %*\r\n");

    script
}

fn write_connect_env_example(
    output_dir: &Path,
    spec: &ConnectProfileSpec,
) -> Result<PathBuf, String> {
    let env_path = output_dir.join(".env.example");
    let mut keys: Vec<String> = spec
        .header_bindings
        .iter()
        .map(|(_header, env_key)| env_key.clone())
        .collect();
    keys.sort();
    keys.dedup();

    let mut body = String::new();
    for key in keys {
        body.push_str(&format!("{key}=\n"));
    }

    fs::write(&env_path, body)
        .map_err(|err| format!("Failed to write {}: {err}", env_path.display()))?;
    Ok(env_path)
}

fn derive_endpoint_profile_name(endpoint: &str) -> String {
    if let Ok(url) = url::Url::parse(endpoint) {
        if let Some(host) = url.host_str() {
            let name = sanitize_artifact_name(host);
            if !name.is_empty() {
                return name;
            }
        }
    }
    "endpoint-profile".to_string()
}

fn header_env_var_name(header_name: &str) -> String {
    let mut out = String::from("MCPWAY_CONNECT_HEADER_");
    for ch in header_name.chars() {
        if ch.is_ascii_alphanumeric() {
            out.push(ch.to_ascii_uppercase());
        } else {
            out.push('_');
        }
    }
    while out.contains("__") {
        out = out.replace("__", "_");
    }
    out.trim_matches('_').to_string()
}

fn extract_template_env_key(value: &str) -> Option<String> {
    if value.starts_with("${") && value.ends_with('}') && value.len() > 3 {
        return Some(value[2..value.len() - 1].to_string());
    }
    None
}

fn shell_single_quote(value: &str) -> String {
    if value.is_empty() {
        return "''".to_string();
    }
    let escaped = value.replace('"', "\\\"");
    format!("\"{escaped}\"")
}

fn escape_for_double_quotes(value: &str) -> String {
    value.replace('\\', "\\\\").replace('"', "\\\"")
}

#[cfg(unix)]
fn make_executable(path: &Path) -> Result<(), String> {
    use std::os::unix::fs::PermissionsExt;
    let mut perms = fs::metadata(path)
        .map_err(|err| format!("Failed to stat {}: {err}", path.display()))?
        .permissions();
    perms.set_mode(0o755);
    fs::set_permissions(path, perms).map_err(|err| {
        format!(
            "Failed to set executable permissions on {}: {err}",
            path.display()
        )
    })?;
    Ok(())
}

#[cfg(not(unix))]
fn make_executable(_path: &Path) -> Result<(), String> {
    Ok(())
}

fn absolute_path(path: &Path) -> Result<PathBuf, String> {
    if path.is_absolute() {
        return Ok(path.to_path_buf());
    }
    let cwd = env::current_dir().map_err(|err| format!("Failed to read cwd: {err}"))?;
    Ok(cwd.join(path))
}

fn unix_timestamp_utc_string() -> String {
    match SystemTime::now().duration_since(UNIX_EPOCH) {
        Ok(duration) => format!("{}", duration.as_secs()),
        Err(_) => "0".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builds_metadata_record() {
        let normalized = NormalizedDefinition {
            artifact_name: "demo".to_string(),
            server_name: "demo".to_string(),
            command: "node".to_string(),
            args: vec!["server.js".to_string()],
            stdio_command: "node server.js".to_string(),
            env_bindings: Vec::new(),
            header_bindings: Vec::new(),
        };

        let output_dir = PathBuf::from("/tmp/mcpway-demo");
        let artifacts = GeneratedArtifacts {
            script_path: PathBuf::from("/tmp/mcpway-demo/bin/demo"),
            wrapper_path: None,
            mcpway_path: None,
            env_example_path: PathBuf::from("/tmp/mcpway-demo/.env.example"),
        };
        let metadata = build_metadata(MetadataBuildInput {
            normalized: &normalized,
            definition_path: Path::new("/tmp/def.json"),
            server_selector: Some("demo".to_string()),
            definition_bytes: b"{}",
            output_dir: &output_dir,
            options: GenerationOptions {
                bundle_mcpway: true,
                compile_wrapper: false,
            },
            artifacts: &artifacts,
        });

        assert_eq!(metadata.mode, ArtifactMode::Generate);
        assert_eq!(metadata.schema_version, "1");
        assert_eq!(metadata.artifact_name, "demo");
        assert!(metadata
            .regenerate_command
            .contains("regenerate --metadata"));
    }

    #[test]
    fn connect_profile_extracts_template_env_key() {
        assert_eq!(
            extract_template_env_key("${MCPWAY_CONNECT_HEADER_AUTHORIZATION}"),
            Some("MCPWAY_CONNECT_HEADER_AUTHORIZATION".to_string())
        );
        assert_eq!(extract_template_env_key("plain"), None);
    }
}
