//! TypeScript code generator for protobuf files using ts-proto

use anyhow::{bail, Context, Result};
use std::path::PathBuf;
use std::process::Command;

/// TypeScript protobuf code generator using ts-proto
pub struct TypeScriptGenerator {
    project_root: PathBuf,
}

impl TypeScriptGenerator {
    /// Create a new TypeScript generator
    pub fn new(project_root: PathBuf) -> Self {
        Self { project_root }
    }

    /// Generate TypeScript code from proto files
    pub fn generate(&self) -> Result<()> {
        let proto_dir = self.project_root.join("protos");
        let output_dir = self.project_root.join("src").join("protos");

        std::fs::create_dir_all(&output_dir)
            .context("Failed to create TypeScript output directory")?;

        self.check_protoc()?;
        self.check_ts_proto()?;

        let proto_files = self.find_proto_files(&proto_dir)?;

        if proto_files.is_empty() {
            println!("⚠️  No .proto files found in {}", proto_dir.display());
            return Ok(());
        }

        println!("📁 Found {} proto file(s)", proto_files.len());

        for proto_file in &proto_files {
            self.generate_typescript(proto_file, &proto_dir, &output_dir)?;
        }

        println!("📝 Generated TypeScript code in {}", output_dir.display());

        Ok(())
    }

    /// Check if protoc is available
    fn check_protoc(&self) -> Result<()> {
        let output = Command::new("protoc").arg("--version").output();

        match output {
            Ok(output) if output.status.success() => {
                let version = String::from_utf8_lossy(&output.stdout);
                println!("✓ Found protoc: {}", version.trim());
                Ok(())
            }
            _ => {
                bail!(
                    "protoc not found. Please install Protocol Buffers compiler:\n\
                     - Windows: choco install protoc / scoop install protobuf\n\
                     - macOS: brew install protobuf\n\
                     - Linux: apt install protobuf-compiler"
                )
            }
        }
    }

    /// Check if ts-proto plugin is available
    fn check_ts_proto(&self) -> Result<()> {
        let plugin_path = self.get_ts_proto_plugin_path();

        if plugin_path.exists() {
            println!("✓ Found ts-proto plugin");
            Ok(())
        } else {
            bail!(
                "ts-proto not found at {}. Please run 'pnpm install' first.",
                plugin_path.display()
            )
        }
    }

    /// Get the ts-proto plugin path based on platform
    fn get_ts_proto_plugin_path(&self) -> PathBuf {
        let base = self.project_root.join("node_modules").join(".bin");

        if cfg!(windows) {
            base.join("protoc-gen-ts_proto.cmd")
        } else {
            base.join("protoc-gen-ts_proto")
        }
    }

    /// Generate TypeScript code for a single proto file
    fn generate_typescript(
        &self,
        proto_file: &PathBuf,
        proto_dir: &PathBuf,
        output_dir: &PathBuf,
    ) -> Result<()> {
        let plugin_path = self.get_ts_proto_plugin_path();

        let plugin_arg = if cfg!(windows) {
            format!("protoc-gen-ts_proto={}", plugin_path.display())
        } else {
            format!("--plugin={}", plugin_path.display())
        };

        let ts_proto_opts = [
            "esModuleInterop=true",
            "outputServices=false",
            "useOptionals=messages",
            "exportCommonSymbols=false",
            "oneof=unions",
            "unrecognizedEnum=false",
        ]
        .join(",");

        let ts_proto_out = format!("--ts_proto_out={}", output_dir.display());
        let ts_proto_opt = format!("--ts_proto_opt={}", ts_proto_opts);
        let proto_path = format!("--proto_path={}", proto_dir.display());

        let mut cmd = Command::new("protoc");

        if cfg!(windows) {
            cmd.arg(format!("--plugin={}", plugin_arg));
        } else {
            cmd.arg(plugin_arg);
        }

        cmd.arg(&ts_proto_out)
            .arg(&ts_proto_opt)
            .arg(&proto_path)
            .arg(proto_file);

        println!(
            "  → Generating from {}",
            proto_file.file_name().unwrap().to_string_lossy()
        );

        let output = cmd.output().context("Failed to execute protoc")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!("protoc failed:\n{}", stderr);
        }

        Ok(())
    }

    /// Find all .proto files in the given directory
    fn find_proto_files(&self, dir: &PathBuf) -> Result<Vec<PathBuf>> {
        let mut proto_files = Vec::new();

        if !dir.exists() {
            return Ok(proto_files);
        }

        for entry in std::fs::read_dir(dir).context("Failed to read proto directory")? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file() && path.extension().is_some_and(|ext| ext == "proto") {
                proto_files.push(path);
            }
        }

        Ok(proto_files)
    }
}
