use crate::meta;
use crate::registry::{CommandMeta, NamespaceMap, ParamMeta, TypeMeta};
use crate::renderer::{render_enum_meta, render_type};
use crate::utils::snake_to_camel;
use std::fs;
use std::path::PathBuf;

/// Controls how function names are automatically cleaned when in a namespace.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NamingStrategy {
    /// Strips the namespace prefix (e.g., `task_create` -> `create`).
    Prefix,
    /// Strips the namespace postfix (e.g., `create_task` -> `create`).
    Postfix,
}

/// Configuration for the TypeScript generator.
#[derive(Debug, Clone, Copy)]
pub struct GeneratorConfig {
    /// How to automatically rename functions in namespaces if no explicit rename is provided.
    pub naming_strategy: NamingStrategy,
    /// Whether to treat all Option/nullable fields as optional in TypeScript.
    pub option_fields_as_optional: bool,
}

impl Default for GeneratorConfig {
    fn default() -> Self {
        Self {
            naming_strategy: NamingStrategy::Prefix,
            option_fields_as_optional: false,
        }
    }
}

/// Builder for TypeScript generation.
#[derive(Debug, Clone)]
pub struct Generator {
    output_path: PathBuf,
    config: GeneratorConfig,
}

impl Generator {
    pub fn new<P: Into<PathBuf>>(output_path: P) -> Self {
        Self {
            output_path: output_path.into(),
            config: GeneratorConfig::default(),
        }
    }

    pub fn option_fields_as_optional(mut self) -> Self {
        self.config.option_fields_as_optional = true;
        self
    }

    pub fn run(self) -> std::io::Result<()> {
        #[cfg(feature = "tauri")]
        {
            generate_with_config(self.output_path.to_str().unwrap(), self.config)
        }
        #[cfg(not(feature = "tauri"))]
        {
            generate_full(
                self.output_path.to_str().unwrap(),
                self.config,
                write_command_declarations,
                |_| {},
            )
        }
    }
}

pub fn strip_naming(name: &str, ns: &str, is_type: bool, strategy: NamingStrategy) -> String {
    if is_type {
        let stripped = name.replace(ns, "");
        if stripped.is_empty() {
            return name.to_string();
        }
        return stripped;
    }

    let ns_norm = ns.to_lowercase();
    let separator = "_";

    match strategy {
        NamingStrategy::Prefix => {
            let prefix = format!("{}{}", ns_norm, separator);
            if name.starts_with(&prefix) && name.len() > prefix.len() {
                name[prefix.len()..].to_string()
            } else {
                name.to_string()
            }
        }
        NamingStrategy::Postfix => {
            let postfix = format!("{}{}", separator, ns_norm);
            if name.ends_with(&postfix) && name.len() > postfix.len() {
                name[..name.len() - postfix.len()].to_string()
            } else {
                name.to_string()
            }
        }
    }
}

/// Generates TypeScript bindings for all registered types, commands, and constants.
///
/// This is the primary entry point for non-Tauri projects. For Tauri projects,
/// use `tyzen_tauri::generate` instead, which also emits typed command wrappers
/// and event listeners.
///
/// Creates parent directories as needed. The file is only written when content
/// has changed, avoiding unnecessary downstream rebuilds.
///
/// # Errors
///
/// Returns an error if the output directory cannot be created or the file
/// cannot be written.
#[cfg(feature = "tauri")]
pub fn generate(output_path: &str) -> std::io::Result<()> {
    generate_with_config(output_path, GeneratorConfig::default())
}

#[cfg(feature = "tauri")]
pub fn generate_with_config(output_path: &str, config: GeneratorConfig) -> std::io::Result<()> {
    use std::fs;
    let path = std::path::Path::new(output_path);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
        fs::write(parent.join("helpers.ts"), TAURI_HELPERS)?;
    }
    generate_full(output_path, config, write_tauri_commands, |ts| {
        write_tauri_events_with_config(ts, config)
    })
}

#[cfg(not(feature = "tauri"))]
pub fn generate(output_path: &str) -> std::io::Result<()> {
    generate_full(
        output_path,
        GeneratorConfig::default(),
        write_command_declarations,
        |_| {},
    )
}

/// Generates TypeScript bindings with a custom command writer.
///
/// Like [`generate`], but allows you to control how command declarations
/// are emitted. Useful when building framework-specific integrations on
/// top of Tyzen.
///
/// # Errors
///
/// Returns an error if the output file cannot be created or written.
pub fn generate_with_commands(
    output_path: &str,
    write_commands: fn(&mut String),
) -> std::io::Result<()> {
    generate_full(
        output_path,
        GeneratorConfig::default(),
        write_commands,
        |_| {},
    )
}

/// Generates TypeScript bindings with full control over the output structure.
///
/// `write_before_types` runs before type definitions (use it for imports and
/// command declarations). `write_after_types` runs last (use it for event
/// helpers or custom utility code).
///
/// The generated file structure is:
/// 1. Header comment
/// 2. `write_before_types` output
/// 3. Exported constants (sorted alphabetically by name)
/// 4. Type definitions (sorted alphabetically by name)
/// 5. Built-in `Result<T, E>` type alias
/// 6. `write_after_types` output
///
/// # Errors
///
/// Returns an error if the output file cannot be created or written.
pub fn generate_full(
    output_path: &str,
    config: GeneratorConfig,
    write_before_types: impl FnOnce(&mut String),
    write_after_types: impl FnOnce(&mut String),
) -> std::io::Result<()> {
    let path = PathBuf::from(output_path);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }

    let map = NamespaceMap::collect();
    let all_types: Vec<&TypeMeta> = map.types.values().flatten().cloned().collect();
    let has_zod = all_types.iter().any(|t| t.schema);

    let mut ts = String::new();
    ts.push_str("// auto-generated by tyzen, do not edit\n\n");
    if has_zod {
        ts.push_str("import { z } from \"zod\";\n\n");
    }
    write_before_types(&mut ts);

    // 1. Global Types (for ergonomics)
    ts.push_str("\n/** Global Types **/\n");
    let mut sorted_all_types = all_types.clone();
    sorted_all_types.sort_by_key(|t| {
        let is_enum = matches!((t.structure)(), meta::TypeStructure::Enum(_));
        (!is_enum, t.name)
    });
    for t in &sorted_all_types {
        ts.push_str(&render_type(t, &all_types, config));
        ts.push('\n');
    }

    // 1b. Enum Metadata (rendered after all type declarations so block-scoped references are safe)
    ts.push_str("\n/** Enum Metadata **/\n");
    for t in &sorted_all_types {
        if let meta::TypeStructure::Enum(e) = (t.structure)() {
            if let Some(meta_obj) = render_enum_meta(t, &e) {
                ts.push_str(&meta_obj);
                ts.push_str("\n\n");
            }
        }
    }

    if let Some(root_consts) = map.consts.get(&None) {
        for c in root_consts {
            ts.push_str(&format!(
                "export const {} = {} as const;\n",
                c.name, c.value
            ));
        }
    }

    // 2. Namespaced Scopes
    for ns in &map.namespaces {
        ts.push_str(&format!("\n/** Namespace: {} **/\n", ns));

        // 2a. Types within namespace (using stripped names)
        if let Some(ns_types) = map.types.get(&Some(ns)) {
            ts.push_str(&format!("export namespace {} {{\n", ns));
            for t in ns_types {
                let stripped_name = strip_naming(t.name, ns, true, config.naming_strategy);
                // Avoid circular reference if stripping leaves the same name
                if stripped_name != t.name {
                    ts.push_str(&format!("  export type {} = {};\n", stripped_name, t.name));
                }
            }
            ts.push_str("}\n");
        }

        // 2b. Merged Action Object (Commands, Events, Constants)
        let ns_commands = map.commands.get(&Some(ns));
        let ns_consts = map.consts.get(&Some(ns));
        let ns_events = map.events.get(&Some(ns));

        if ns_commands.is_some() || ns_consts.is_some() || ns_events.is_some() {
            ts.push_str(&format!("export const {} = {{\n", ns));

            if let Some(consts) = ns_consts {
                for c in consts {
                    ts.push_str(&format!("  {}: {} as const,\n", c.name, c.value));
                }
            }

            if let Some(commands) = ns_commands {
                for cmd in commands {
                    let fn_name = cmd.rename.map(|r| r.to_string()).unwrap_or_else(|| {
                        let base = strip_naming(cmd.name, ns, false, config.naming_strategy);
                        snake_to_camel(&base)
                    });
                    let params: Vec<String> = cmd
                        .params
                        .iter()
                        .map(|p| {
                            format!(
                                "{}: {}",
                                snake_to_camel(clean_param_name(p.name)),
                                p.ts_type()
                            )
                        })
                        .collect();
                    ts.push_str(&format!(
                        "  {}: ({}) => __invoke<{}>(\"{}\", {{ {} }}),\n",
                        fn_name,
                        params.join(", "),
                        (cmd.return_type)(),
                        cmd.name,
                        cmd.params
                            .iter()
                            .map(command_arg_expr)
                            .collect::<Vec<_>>()
                            .join(", ")
                    ));
                }
            }

            if let Some(events) = ns_events {
                for ev in events {
                    let camel_base = strip_naming(ev.name, ns, false, config.naming_strategy);
                    let camel_name =
                        snake_to_camel(camel_base.replace("-", "_").replace(":", "_").as_str());
                    let ev_fn_name = format!(
                        "on{}{}",
                        (&camel_name[0..1]).to_uppercase(),
                        &camel_name[1..]
                    );
                    ts.push_str(&format!(
                        "  {}: (cb: (payload: {}) => void) => __listen(\"{}\", cb),\n",
                        ev_fn_name,
                        (ev.payload_type)(),
                        ev.name
                    ));
                }
            }

            // Also include Enum Metadata if a type with the same name exists
            if let Some(ns_types) = map.types.get(&Some(ns)) {
                if let Some(matching_type) = ns_types.iter().find(|t| t.name == *ns) {
                    if let meta::TypeStructure::Enum(e) = (matching_type.structure)() {
                        if !e.variants.iter().all(|v| v.attrs.is_empty()) {
                            ts.push_str("  Meta: {\n");
                            for v in e.variants {
                                let mut attrs: Vec<_> = v.attrs.iter().collect();
                                attrs.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));

                                let mut attrs_str_parts = Vec::new();
                                for (k, val) in attrs {
                                    let k_camel = snake_to_camel(k);
                                    let val_rendered = match val {
                                        meta::AttrValue::Str(s) => format!("\"{}\"", s),
                                        meta::AttrValue::List(items) => {
                                            let ts_items: Vec<String> = items
                                                .iter()
                                                .map(|item| item.replace("::", "."))
                                                .collect();
                                            format!("[{}]", ts_items.join(", "))
                                        }
                                    };
                                    attrs_str_parts.push(format!("{}: {}", k_camel, val_rendered));
                                }

                                let attrs_str = attrs_str_parts.join(", ");
                                if attrs_str.is_empty() {
                                    ts.push_str(&format!("    {}: {{}},\n", v.name));
                                } else {
                                    ts.push_str(&format!("    {}: {{ {} }},\n", v.name, attrs_str));
                                }
                            }
                            ts.push_str("  } as const,\n");
                        }
                    }
                }
            }

            ts.push_str("} as const;\n");
        }
    }

    ts.push('\n');
    ts.push_str("/** Generic Result type used for command returns **/\n");
    ts.push_str("export type Result<T, E = string> = { status: \"ok\"; data: T } | { status: \"error\"; error: E }\n");

    write_after_types(&mut ts);

    let current_content = fs::read_to_string(&path).ok();
    if current_content.as_ref() != Some(&ts) {
        fs::write(&path, ts)?;
        println!(
            "\x1b[32m\x1b[1m{:>12}\x1b[0m bindings updated -> \x1b[90m{}\x1b[0m",
            "Tyzen", output_path
        );
    } else {
        println!(
            "\x1b[33m\x1b[1m{:>12}\x1b[0m bindings are up to date",
            "Tyzen"
        );
    }
    Ok(())
}

/// Writes framework-agnostic TypeScript command type declarations.
///
/// Emits a `Commands` type whose methods match each registered `#[tyzen::command]`,
/// mapping Rust snake_case names to TypeScript camelCase.
///
/// This is used internally by [`generate`] and can be passed to
/// [`generate_full`] as the `write_before_types` callback.
pub fn write_command_declarations(ts: &mut String) {
    let mut commands: Vec<_> = inventory::iter::<CommandMeta>().collect();
    if commands.is_empty() {
        return;
    }
    commands.sort_by_key(|c| c.name);

    ts.push_str("/** autogen commands **/\n");
    ts.push_str("export type Commands = {\n");
    for cmd in commands {
        let fn_name = snake_to_camel(cmd.name);
        let params_ts: Vec<String> = cmd
            .params
            .iter()
            .map(|p| {
                format!(
                    "{}: {}",
                    snake_to_camel(clean_param_name(p.name)),
                    p.ts_type()
                )
            })
            .collect();

        ts.push_str(&format!(
            "  {}({}): Promise<{}>\n",
            fn_name,
            params_ts.join(", "),
            (cmd.return_type)(),
        ));
    }
    ts.push_str("}\n");
}

fn clean_param_name(name: &str) -> &str {
    if name.starts_with('_') && name.len() > 1 {
        &name[1..]
    } else {
        name
    }
}

fn command_arg_expr(param: &ParamMeta) -> String {
    let arg_name = snake_to_camel(clean_param_name(param.name));
    if param.is_channel {
        format!("{}: new Channel({})", arg_name, arg_name)
    } else {
        arg_name
    }
}

#[cfg(feature = "tauri")]
pub const TAURI_HELPERS: &str = r#"import { invoke } from "@tauri-apps/api/core"
import { listen } from "@tauri-apps/api/event"
import type { Result } from "./index"

export type InvokeResult<T> = T extends { status: "ok" } | { status: "error" } ? T : Result<T>;

export async function __invoke<T>(name: string, args: Record<string, any>): Promise<InvokeResult<T>> {
  try {
    const data = await invoke(name, args);
    return { status: "ok", data } as unknown as InvokeResult<T>;
  } catch (e) {
    if (e instanceof Error) throw e;
    return { status: "error", error: e as string } as unknown as InvokeResult<T>;
  }
}

export function __listen(name: string, cb: (payload: any) => void) {
  return listen(name, (e) => cb(e.payload));
}

export function parseError<
  T extends { tag: string; value?: any },
  M extends Record<string, any>
>(
  error: T,
  meta: M
): { message: string; code: string } & (T["tag"] extends keyof M
  ? M[T["tag"]]
  : Record<string, unknown>) {
  const variantMeta = (meta as any)[error.tag] || {};
  let message: string = variantMeta.msg || "An unknown error occurred";
  const code: string = variantMeta.code || "UNKNOWN_ERROR";

  if (error.value !== undefined && typeof error.value === 'object') {
    Object.entries(error.value).forEach(([key, val]) => {
      message = message.replace(`{${key}}`, String(val));
    });
  } else if (error.value !== undefined) {
    message = message.replace("{value}", String(error.value));
    message = message.replace("{0}", String(error.value));
  }

  return { message, code, ...variantMeta } as any;
}

export function toBinary(data: unknown): Uint8Array {
  if (data instanceof Uint8Array) return data;
  if (Array.isArray(data)) return new Uint8Array(data);
  if (typeof data === "string") {
    const bin = atob(data);
    const bytes = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
    return bytes;
  }
  return new Uint8Array(0);
}
"#;

#[cfg(feature = "tauri")]
pub fn write_tauri_commands(ts: &mut String) {
    let map = NamespaceMap::collect();
    let has_channels = map
        .commands
        .values()
        .flatten()
        .any(|cmd| cmd.params.iter().any(|param| param.is_channel));

    ts.push_str("import { __invoke, toBinary } from \"./helpers\"\n");
    if has_channels {
        ts.push_str("import { Channel } from \"@tauri-apps/api/core\"\n");
    }
    ts.push_str("\n");

    if let Some(root_commands) = map.commands.get(&None) {
        ts.push_str("export const commands = {\n");
        for cmd in root_commands {
            let fn_name = cmd
                .rename
                .map(|r| r.to_string())
                .unwrap_or_else(|| snake_to_camel(cmd.name));
            let params_ts: Vec<String> = cmd
                .params
                .iter()
                .map(|p| {
                    format!(
                        "{}: {}",
                        snake_to_camel(clean_param_name(p.name)),
                        p.ts_type()
                    )
                })
                .collect();
            let ret_ty = (cmd.return_type)();

            let mut transformer = "".to_string();
            if cmd.is_binary {
                transformer = ".then(res => { if (res.status === 'ok') res.data = toBinary(res.data); return res; })".to_string();
            } else {
                let all_types: Vec<&TypeMeta> = map.types.values().flatten().cloned().collect();
                if let Some(target) = all_types.iter().find(|t| t.name == ret_ty) {
                    if target.has_binary {
                        transformer = format!(
                            ".then(res => {{ if (res.status === 'ok') (res as any).data = to{}(res.data); return res; }})",
                            target.name
                        );
                    }
                }
            }

            ts.push_str(&format!(
                "  {}: ({}) => __invoke<{}>(\"{}\", {{ {} }}){},\n",
                fn_name,
                params_ts.join(", "),
                ret_ty,
                cmd.name,
                cmd.params
                    .iter()
                    .map(command_arg_expr)
                    .collect::<Vec<_>>()
                    .join(", "),
                transformer
            ));
        }
        ts.push_str("}\n");
    }
}

#[cfg(feature = "tauri")]
pub fn write_tauri_events(ts: &mut String) {
    write_tauri_events_with_config(ts, GeneratorConfig::default())
}

#[cfg(feature = "tauri")]
pub fn write_tauri_events_with_config(ts: &mut String, _config: GeneratorConfig) {
    let map = NamespaceMap::collect();

    ts.push_str("import { __listen } from \"./helpers\"\n");

    if let Some(root_events) = map.events.get(&None) {
        ts.push_str("\n/** Global Events **/\n");
        ts.push_str("export const events = {\n");
        for ev in root_events {
            let camel_name = snake_to_camel(ev.name.replace("-", "_").replace(":", "_").as_str());
            let ev_fn_name = format!(
                "on{}{}",
                (&camel_name[0..1]).to_uppercase(),
                &camel_name[1..]
            );
            ts.push_str(&format!(
                "  {}: (cb: (payload: {}) => void) => __listen(\"{}\", cb),\n",
                ev_fn_name,
                (ev.payload_type)(),
                ev.name
            ));
        }
        ts.push_str("}\n");
    }
}
