use crate::utils::{find_up, FoundryConfig};
use crate::{lookup, Bytes32, Info, EVM};
use anyhow::{anyhow, Context, Result};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
use zinkc::{Artifact, Compiler, Config, Constructor, InitStorage};

/// Represents the bytecode object in Foundry output
#[derive(Deserialize)]
struct BytecodeObject {
    object: String,
}

/// Represents a Foundry output JSON file (e.g., out/Storage.sol/Storage.json)
#[derive(Deserialize)]
pub struct FoundryOutput {
    bytecode: BytecodeObject,
}

impl FoundryOutput {
    /// Get the bytecode as a string
    pub fn bytecode(&self) -> &str {
        &self.bytecode.object
    }
}

/// Contract instance for testing.
#[derive(Default)]
pub struct Contract {
    /// If enable dispatcher.
    pub dispatcher: bool,
    /// The artifact of the contract.
    pub artifact: Artifact,
    /// The source WASM of the contract.
    pub wasm: Vec<u8>,
    /// Bytecode constructor
    pub constructor: Constructor,
    /// Address in evm
    pub address: [u8; 20],
}

impl<T> From<T> for Contract
where
    T: AsRef<[u8]>,
{
    fn from(wasm: T) -> Self {
        crate::setup_logger();

        Self {
            wasm: wasm.as_ref().into(),
            dispatcher: true,
            ..Default::default()
        }
    }
}

impl Contract {
    /// Locate Foundry outputs and return a list of (contract_name, abi_path, bytecode)
    pub fn find_foundry_outputs() -> Result<Vec<(String, PathBuf, Vec<u8>)>> {
        // Find foundry.toml
        let foundry_toml_path = find_up("foundry.toml")?;
        let foundry_toml_content =
            fs::read_to_string(&foundry_toml_path).context("Failed to read foundry.toml")?;
        let foundry_config: FoundryConfig =
            toml::from_str(&foundry_toml_content).context("Failed to parse foundry.toml")?;

        // Determine the output directory
        let out_dir = foundry_config
            .profile
            .default
            .out
            .unwrap_or_else(|| "out".to_string());
        let out_path = foundry_toml_path.parent().unwrap().join(&out_dir);

        let mut outputs = Vec::new();
        for entry in fs::read_dir(&out_path).context("Failed to read Foundry out directory")? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                // Skip the build-info directory
                if path.file_name().and_then(|s| s.to_str()) == Some("build-info") {
                    continue;
                }
                // Look for .json files in subdirectories (e.g., out/Storage.sol/)
                for sub_entry in fs::read_dir(&path)? {
                    let sub_entry = sub_entry.context("Failed to read directory entry")?;
                    let sub_path = sub_entry.path();
                    if sub_path.extension().and_then(|s| s.to_str()) == Some("json") {
                        let file_name = sub_path
                            .file_name()
                            .context("Failed to get file name")?
                            .to_str()
                            .context("File name is not valid UTF-8")?;
                        let Some(contract_name) = file_name.strip_suffix(".json") else {
                            continue;
                        };
                        // Only process files where the contract name (before .json) looks like a contract name
                        if contract_name
                            .chars()
                            .all(|c| c.is_alphanumeric() || c == '_')
                        {
                            let content = fs::read_to_string(&sub_path)?;
                            let output: FoundryOutput = serde_json::from_str(&content)
                                .context(format!("Failed to parse JSON for {file_name}"))?;
                            let bytecode = hex::decode(output.bytecode().trim_start_matches("0x"))
                                .context("Failed to decode bytecode")?;
                            outputs.push((contract_name.to_string(), sub_path, bytecode));
                        }
                    }
                }
            }
        }

        Ok(outputs)
    }

    /// Get the bytecode of the contract.
    pub fn bytecode(&self) -> Result<Vec<u8>> {
        let bytecode = self
            .constructor
            .finish(self.artifact.runtime_bytecode.clone().into())
            .map(|v| v.to_vec())?;

        tracing::debug!("runtime bytecode: {}", hex::encode(&bytecode));
        Ok(bytecode)
    }

    /// Preset the storage of the contract, similar with the concept `constructor`
    /// in solidity, but just in time.
    pub fn construct(&mut self, storage: InitStorage) -> Result<&mut Self> {
        self.constructor.storage(storage)?;
        Ok(self)
    }

    /// Compile WASM to EVM bytecode.
    pub fn compile(mut self) -> Result<Self> {
        let config = Config::default().dispatcher(self.dispatcher);
        let compiler = Compiler::new(config);
        self.artifact = compiler.compile(&self.wasm)?;

        // tracing::debug!("abi: {:#}", self.json_abi()?);
        tracing::debug!("bytecode: {}", hex::encode(&self.artifact.runtime_bytecode));
        Ok(self)
    }

    /// Deploy self to evm
    pub fn deploy<'e>(&mut self) -> Result<EVM<'e>> {
        let mut evm = EVM::default();
        let info = evm.deploy(&self.bytecode()?)?;

        self.address.copy_from_slice(&info.address);
        Ok(evm)
    }

    /// Load zink contract defined in the current
    /// package.
    ///
    /// NOTE: This only works if the current contract
    /// is not an example.
    pub fn current() -> Result<Self> {
        Self::search(&lookup::pkg_name()?)
    }

    /// Encode call data
    pub fn encode<Param>(&self, inputs: impl AsRef<[Param]>) -> Result<Vec<u8>>
    where
        Param: Bytes32,
    {
        let mut calldata = Vec::new();
        let mut inputs = inputs.as_ref();
        if self.dispatcher {
            if inputs.is_empty() {
                return Err(anyhow!("no selector provided"));
            }

            calldata.extend_from_slice(&zabi::selector::parse(&inputs[0].to_vec()));
            inputs = &inputs[1..];
        }

        for input in inputs {
            calldata.extend_from_slice(&input.to_bytes32());
        }

        tracing::debug!("calldata: {}", hex::encode(&calldata));
        Ok(calldata)
    }

    /// Execute the contract.
    pub fn execute<Param>(&mut self, inputs: impl AsRef<[Param]>) -> Result<Info>
    where
        Param: Bytes32,
    {
        EVM::interp(&self.artifact.runtime_bytecode, &self.encode(inputs)?)
    }

    /// Get the JSON ABI of the contract.
    pub fn json_abi(&self) -> Result<String> {
        serde_json::to_string_pretty(&self.artifact.abi).map_err(Into::into)
    }

    /// Disable dispatcher.
    pub fn pure(mut self) -> Self {
        self.dispatcher = false;
        self
    }

    /// Search for zink contract in the target directory.
    pub fn search(name: &str) -> Result<Self> {
        // TODO(g4tianx): `Contract::search` to fail properly
        // when the contract file isn’t found
        crate::setup_logger();
        let wasm = lookup::wasm(name)?;
        zinkc::utils::wasm_opt(&wasm, &wasm)?;

        tracing::debug!("loading contract from {}", wasm.display());
        Ok(Self::from(fs::read(wasm)?))
    }
}