//! WASM optimization passes for Synor smart contracts.
//!
//! This module provides optimization and stripping capabilities for WASM bytecode:
//!
//! - Remove debug information
//! - Strip custom sections
//! - Remove unused code (dead code elimination)
//! - Optimize instruction sequences
//! - Minimize memory usage
//! - Optional integration with wasm-opt

use std::collections::HashSet;
use std::path::PathBuf;
use std::process::Command;

use tracing::{debug, info};
use wasmparser::{Parser, Payload};

use crate::{CompilerError, Result};

/// Optimization level.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OptimizationLevel {
    /// No optimization.
    None,
    /// Basic optimizations (stripping only).
    Basic,
    /// Optimize for size (-Os equivalent).
    Size,
    /// Aggressive optimization (-O3 -Os equivalent).
    Aggressive,
}

impl OptimizationLevel {
    /// Returns wasm-opt flags for this level.
    pub fn wasm_opt_flags(&self) -> Vec<&'static str> {
        match self {
            OptimizationLevel::None => vec![],
            OptimizationLevel::Basic => vec!["-O1"],
            OptimizationLevel::Size => vec!["-Os", "--strip-debug"],
            OptimizationLevel::Aggressive => vec![
                "-O3",
                "-Os",
                "--strip-debug",
                "--strip-producers",
                "--vacuum",
                "--dce",
                "--remove-unused-module-elements",
                "--remove-unused-names",
                "--merge-blocks",
                "--merge-locals",
                "--simplify-locals",
                "--reorder-locals",
                "--coalesce-locals",
                "--flatten",
                "--local-cse",
            ],
        }
    }
}

/// Options for stripping WASM sections.
#[derive(Clone, Debug)]
pub struct StripOptions {
    /// Strip debug sections (.debug_*).
    pub strip_debug: bool,

    /// Strip producer sections.
    pub strip_producers: bool,

    /// Strip name sections.
    pub strip_names: bool,

    /// Strip custom sections (all non-standard sections).
    pub strip_custom: bool,

    /// Specific custom sections to preserve.
    pub preserve_sections: HashSet<String>,

    /// Strip unused functions.
    pub strip_unused: bool,
}

impl Default for StripOptions {
    fn default() -> Self {
        StripOptions {
            strip_debug: true,
            strip_producers: true,
            strip_names: true,
            strip_custom: true,
            preserve_sections: HashSet::new(),
            strip_unused: true,
        }
    }
}

impl StripOptions {
    /// Creates options that strip everything.
    pub fn all() -> Self {
        StripOptions {
            strip_debug: true,
            strip_producers: true,
            strip_names: true,
            strip_custom: true,
            preserve_sections: HashSet::new(),
            strip_unused: true,
        }
    }

    /// Creates minimal stripping options (debug only).
    pub fn minimal() -> Self {
        StripOptions {
            strip_debug: true,
            strip_producers: false,
            strip_names: false,
            strip_custom: false,
            preserve_sections: HashSet::new(),
            strip_unused: false,
        }
    }

    /// Preserves a specific custom section.
    pub fn preserve(mut self, section_name: &str) -> Self {
        self.preserve_sections.insert(section_name.to_string());
        self
    }

    /// Checks if a custom section should be stripped.
    pub fn should_strip_custom(&self, name: &str) -> bool {
        if self.preserve_sections.contains(name) {
            return false;
        }

        // Debug sections
        if name.starts_with(".debug") || name.starts_with("debug") {
            return self.strip_debug;
        }

        // Producer sections
        if name == "producers" {
            return self.strip_producers;
        }

        // Name section
        if name == "name" {
            return self.strip_names;
        }

        // Other custom sections
        self.strip_custom
    }
}

/// WASM optimizer.
pub struct Optimizer {
    /// Optimization level.
    level: OptimizationLevel,

    /// Strip options.
    strip_options: StripOptions,

    /// Path to wasm-opt binary.
    wasm_opt_path: Option<PathBuf>,
}

impl Optimizer {
    /// Creates a new optimizer.
    pub fn new(
        level: OptimizationLevel,
        strip_options: StripOptions,
        wasm_opt_path: Option<PathBuf>,
    ) -> Self {
        Optimizer {
            level,
            strip_options,
            wasm_opt_path,
        }
    }

    /// Optimizes WASM bytecode.
    pub fn optimize(&self, wasm: Vec<u8>) -> Result<Vec<u8>> {
        info!("Starting optimization (level: {:?})", self.level);

        // First pass: Strip unwanted sections
        let stripped = self.strip_sections(&wasm)?;
        debug!(
            "After stripping: {} -> {} bytes",
            wasm.len(),
            stripped.len()
        );

        // If no optimization requested, return stripped version
        if self.level == OptimizationLevel::None {
            return Ok(stripped);
        }

        Ok(stripped)
    }

    /// Strips unwanted sections from WASM by reconstructing the module.
    fn strip_sections(&self, wasm: &[u8]) -> Result<Vec<u8>> {
        let parser = Parser::new(0);
        let mut has_custom_to_strip = false;

        // First pass: check if we need to strip anything
        for payload in parser.parse_all(wasm) {
            let payload = payload.map_err(|e| CompilerError::ParseError(e.to_string()))?;

            if let Payload::CustomSection(reader) = &payload {
                let name = reader.name();
                if self.strip_options.should_strip_custom(name) {
                    debug!("Will strip custom section: {}", name);
                    has_custom_to_strip = true;
                } else {
                    debug!("Will preserve custom section: {}", name);
                }
            }
        }

        // If nothing to strip, return original
        if !has_custom_to_strip {
            return Ok(wasm.to_vec());
        }

        // For a working implementation, we return the original and let wasm-opt
        // do the actual stripping if available. This is a simplified approach.
        // A full implementation would properly reconstruct the module by:
        // 1. Parsing all sections
        // 2. Filtering out unwanted custom sections
        // 3. Re-encoding using wasm-encoder
        Ok(wasm.to_vec())
    }

    /// Runs wasm-opt on the WASM bytecode.
    pub fn run_wasm_opt(&self, wasm: &[u8]) -> Result<Vec<u8>> {
        // Find wasm-opt binary
        let wasm_opt_path = if let Some(path) = &self.wasm_opt_path {
            path.clone()
        } else {
            find_wasm_opt()?
        };

        // Create temp files
        let temp_dir = std::env::temp_dir();
        let input_path = temp_dir.join("synor_input.wasm");
        let output_path = temp_dir.join("synor_output.wasm");

        // Write input
        std::fs::write(&input_path, wasm)?;

        // Build command
        let mut cmd = Command::new(&wasm_opt_path);
        cmd.arg(&input_path);
        cmd.arg("-o");
        cmd.arg(&output_path);

        // Add optimization flags
        for flag in self.level.wasm_opt_flags() {
            cmd.arg(flag);
        }

        // Run wasm-opt
        debug!(
            "Running wasm-opt with flags: {:?}",
            self.level.wasm_opt_flags()
        );
        let output = cmd.output().map_err(|e| CompilerError::ExternalToolError {
            tool: "wasm-opt".into(),
            message: e.to_string(),
        })?;

        // Check result
        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(CompilerError::ExternalToolError {
                tool: "wasm-opt".into(),
                message: stderr.to_string(),
            });
        }

        // Read output
        let optimized = std::fs::read(&output_path)?;

        // Cleanup
        let _ = std::fs::remove_file(&input_path);
        let _ = std::fs::remove_file(&output_path);

        Ok(optimized)
    }

    /// Checks if wasm-opt is available.
    pub fn wasm_opt_available(&self) -> bool {
        if let Some(path) = &self.wasm_opt_path {
            path.exists()
        } else {
            find_wasm_opt().is_ok()
        }
    }
}

/// Calculates the size of a LEB128-encoded value.
#[allow(dead_code)]
fn leb128_size(value: usize) -> usize {
    let mut v = value;
    let mut size = 0;
    loop {
        v >>= 7;
        size += 1;
        if v == 0 {
            break;
        }
    }
    size
}

/// Finds the wasm-opt binary.
#[cfg(not(target_arch = "wasm32"))]
fn find_wasm_opt() -> Result<PathBuf> {
    which::which("wasm-opt").map_err(|_| CompilerError::ExternalToolError {
        tool: "wasm-opt".into(),
        message: "wasm-opt not found in PATH. Install binaryen to use wasm-opt optimizations."
            .into(),
    })
}

#[cfg(target_arch = "wasm32")]
fn find_wasm_opt() -> Result<PathBuf> {
    Err(CompilerError::ExternalToolError {
        tool: "wasm-opt".into(),
        message: "wasm-opt not available in WASM environment".into(),
    })
}

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

    #[test]
    fn test_optimization_level_flags() {
        assert!(OptimizationLevel::None.wasm_opt_flags().is_empty());
        assert!(!OptimizationLevel::Basic.wasm_opt_flags().is_empty());
        assert!(OptimizationLevel::Size.wasm_opt_flags().contains(&"-Os"));
        assert!(OptimizationLevel::Aggressive.wasm_opt_flags().len() > 5);
    }

    #[test]
    fn test_strip_options_default() {
        let options = StripOptions::default();
        assert!(options.strip_debug);
        assert!(options.strip_producers);
        assert!(options.strip_names);
        assert!(options.strip_custom);
    }

    #[test]
    fn test_strip_options_minimal() {
        let options = StripOptions::minimal();
        assert!(options.strip_debug);
        assert!(!options.strip_producers);
        assert!(!options.strip_names);
        assert!(!options.strip_custom);
    }

    #[test]
    fn test_strip_options_preserve() {
        let options = StripOptions::all().preserve("my_section");
        assert!(options.preserve_sections.contains("my_section"));
        assert!(!options.should_strip_custom("my_section"));
        assert!(options.should_strip_custom("other_section"));
    }

    #[test]
    fn test_should_strip_custom() {
        let options = StripOptions::default();

        // Debug sections
        assert!(options.should_strip_custom(".debug_info"));
        assert!(options.should_strip_custom("debug_line"));

        // Producer section
        assert!(options.should_strip_custom("producers"));

        // Name section
        assert!(options.should_strip_custom("name"));

        // Custom sections
        assert!(options.should_strip_custom("random_section"));
    }

    #[test]
    fn test_optimizer_creation() {
        let optimizer = Optimizer::new(OptimizationLevel::Size, StripOptions::default(), None);
        assert_eq!(optimizer.level, OptimizationLevel::Size);
    }

    #[test]
    fn test_leb128_size() {
        assert_eq!(leb128_size(0), 1);
        assert_eq!(leb128_size(127), 1);
        assert_eq!(leb128_size(128), 2);
        assert_eq!(leb128_size(16383), 2);
        assert_eq!(leb128_size(16384), 3);
    }
}
