use clap::{Parser, Subcommand};
use eyre::{Context, Result};
use newton_prover_core::config::NewtonAvsConfig;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing;

use crate::config::NewtonCliConfig;

/// Policy files commands
#[derive(Debug, Parser)]
#[command(name = "policy-files")]
pub struct PolicyFilesCommand {
    #[command(subcommand)]
    pub subcommand: PolicyFilesSubcommand,
}

#[derive(Debug, Subcommand)]
pub enum PolicyFilesSubcommand {
    /// Generate CIDs for policy files
    #[command(name = "generate-cids")]
    GenerateCids(GenerateCidsCommand),
}

/// Generate CIDs command
#[derive(Debug, Parser)]
pub struct GenerateCidsCommand {
    /// Directory containing policy files (defaults to policy-files)
    #[arg(short, long, default_value = "policy-files")]
    directory: PathBuf,

    /// Entrypoint (e.g., "newton_trading_agent.allow")
    #[arg(long)]
    entrypoint: String,

    /// Pinata JWT (or set PINATA_JWT env var)
    #[arg(long, env = "PINATA_JWT")]
    pinata_jwt: Option<String>,

    /// Pinata Gateway (or set PINATA_GATEWAY env var)
    #[arg(long, env = "PINATA_GATEWAY")]
    pinata_gateway: Option<String>,

    /// Output file path (defaults to policy-files/policy_cids.json)
    #[arg(short, long, default_value = "policy-files/policy_cids.json")]
    output: PathBuf,
}

#[derive(Debug, Serialize, Deserialize)]
#[allow(non_snake_case)]
struct PolicyCids {
    wasmCid: String,
    wasmArgs: String,
    policyCid: String,
    schemaCid: String,
    attester: String,
    entrypoint: String,
    policyMetadataCid: String,
    policyDataMetadataCid: String,
}

#[derive(Debug)]
struct FileUpload {
    name: String,
    path: PathBuf,
    mime_type: String,
    default_name_prefix: String,
    header: String,
    json_key: String, // Key in the PolicyCids struct
}

impl GenerateCidsCommand {
    /// Upload a file to Pinata IPFS
    pub async fn upload_file_to_pinata(
        file: PathBuf,
        jwt: String,
        file_name: String,
        mime_type: &str,
    ) -> Result<String> {
        // Check if file exists
        if !file.exists() {
            eyre::bail!("Error: file not found: {:?}", file);
        }

        // Read file
        let file_data = std::fs::read(&file).with_context(|| format!("Failed to read file: {:?}", file))?;

        // Get file name from path
        let file_name_from_path = file.file_name().and_then(|n| n.to_str()).unwrap_or("file");

        // Create multipart form
        let form = reqwest::multipart::Form::new()
            .text("pinataOptions", r#"{"cidVersion":1}"#)
            .text("pinataMetadata", format!(r#"{{"name":"{}"}}"#, file_name))
            .part(
                "file",
                reqwest::multipart::Part::bytes(file_data)
                    .file_name(file_name_from_path.to_string())
                    .mime_str(mime_type)
                    .unwrap(),
            );

        // Upload to Pinata
        let client = reqwest::Client::new();
        let response = client
            .post("https://api.pinata.cloud/pinning/pinFileToIPFS")
            .header("Authorization", format!("Bearer {}", jwt))
            .multipart(form)
            .send()
            .await
            .context("Failed to send request to Pinata")?;

        if !response.status().is_success() {
            let status = response.status();
            let error_text = response.text().await.unwrap_or_default();
            eyre::bail!("Pinata upload failed with status {}: {}", status, error_text);
        }

        // Parse response
        let response_json: serde_json::Value = response.json().await.context("Failed to parse Pinata response")?;

        // Extract IPFS hash
        let ipfs_hash = response_json
            .get("IpfsHash")
            .and_then(|v| v.as_str())
            .ok_or_else(|| eyre::eyre!("IPFS hash not found in response"))?;

        Ok(ipfs_hash.to_string())
    }

    /// Display upload results
    pub fn display_upload_results(ipfs_hash: &str, gateway: &str) {
        tracing::info!("\n=== IPFS Upload Results ===");
        tracing::info!("IPFS Hash: {}", ipfs_hash);
        // Construct gateway links
        let gateway_link = if gateway.ends_with('/') {
            format!("{}{}", gateway, ipfs_hash)
        } else {
            format!("{}/{}", gateway, ipfs_hash)
        };

        tracing::info!(gateway_link = %gateway_link, "Direct IPFS Link");
        tracing::info!("Public IPFS Link: https://ipfs.io/ipfs/{}", ipfs_hash);
    }

    /// Execute the generate-cids command
    pub async fn execute(self: Box<Self>, _config: NewtonAvsConfig<NewtonCliConfig>) -> Result<()> {
        // Get Pinata credentials
        let jwt = self
            .pinata_jwt
            .ok_or_else(|| eyre::eyre!("Pinata JWT is required. Set PINATA_JWT env var or use --pinata-jwt"))?;

        let gateway = self.pinata_gateway.ok_or_else(|| {
            eyre::eyre!("Pinata Gateway is required. Set PINATA_GATEWAY env var or use --pinata-gateway")
        })?;

        // Define all files to upload in the correct order
        let files_to_upload = vec![
            FileUpload {
                name: "policy.wasm".to_string(),
                path: self.directory.join("policy.wasm"),
                mime_type: "application/wasm".to_string(),
                default_name_prefix: "newton-policy-wasm".to_string(),
                header: "============ Upload policy.wasm ================".to_string(),
                json_key: "wasmCid".to_string(),
            },
            FileUpload {
                name: "wasm_args.json".to_string(),
                path: self.directory.join("wasm_args.json"),
                mime_type: "application/json".to_string(),
                default_name_prefix: "newton-policy-wasm-args".to_string(),
                header: "========== Upload wasm_args.json ===============".to_string(),
                json_key: "wasmArgs".to_string(),
            },
            FileUpload {
                name: "policy.rego".to_string(),
                path: self.directory.join("policy.rego"),
                mime_type: "text/plain".to_string(),
                default_name_prefix: "newton-policy".to_string(),
                header: "============== Upload policy.rego ==============".to_string(),
                json_key: "policyCid".to_string(),
            },
            FileUpload {
                name: "params_schema.json".to_string(),
                path: self.directory.join("params_schema.json"),
                mime_type: "application/json".to_string(),
                default_name_prefix: "newton-policy-schema".to_string(),
                header: "========== Upload params_schema.json ===========".to_string(),
                json_key: "schemaCid".to_string(),
            },
            FileUpload {
                name: "policy_metadata.json".to_string(),
                path: self.directory.join("policy_metadata.json"),
                mime_type: "application/json".to_string(),
                default_name_prefix: "newton-policy-metadata".to_string(),
                header: "========== Upload policy_metadata.json =========".to_string(),
                json_key: "policyMetadataCid".to_string(),
            },
            FileUpload {
                name: "policy_data_metadata.json".to_string(),
                path: self.directory.join("policy_data_metadata.json"),
                mime_type: "application/json".to_string(),
                default_name_prefix: "newton-policy-data-metadata".to_string(),
                header: "======== Upload policy_data_metadata.json ======".to_string(),
                json_key: "policyDataMetadataCid".to_string(),
            },
        ];

        tracing::info!("================================================");
        tracing::info!("========== Uploading All Policy Files ==========");
        tracing::info!("================================================");
        tracing::info!("Directory: {:?}", self.directory);
        tracing::info!("");
        let mut ipfs_hashes: std::collections::HashMap<String, String> = std::collections::HashMap::new();
        // Upload each file and collect IPFS hashes
        for file_upload in files_to_upload {
            tracing::info!("================================================");
            tracing::info!("{}", file_upload.header);
            tracing::info!("================================================");

            // Check if file exists
            if !file_upload.path.exists() {
                // For wasm_args.json, it's optional, so use empty string
                if file_upload.name == "wasm_args.json" {
                    tracing::warn!("Warning: {} not found, using empty string...", file_upload.name);
                    ipfs_hashes.insert(file_upload.json_key.clone(), String::new());
                    continue;
                }
                eyre::bail!("Error: {} not found in {:?}", file_upload.name, self.directory);
            }

            // Generate file name
            let file_name = format!(
                "{}-{}",
                file_upload.default_name_prefix,
                chrono::Utc::now().format("%Y%m%d-%H%M%S")
            );

            tracing::info!(path = %file_upload.path.display(), "Uploading to Pinata IPFS");

            match Self::upload_file_to_pinata(file_upload.path.clone(), jwt.clone(), file_name, &file_upload.mime_type)
                .await
            {
                Ok(ipfs_hash) => {
                    Self::display_upload_results(&ipfs_hash, &gateway);
                    ipfs_hashes.insert(file_upload.json_key.clone(), ipfs_hash);
                }
                Err(e) => {
                    eyre::bail!("Error uploading {}: {}", file_upload.name, e);
                }
            }
            tracing::info!("");
        }

        // Create PolicyCids struct
        let policy_cids = PolicyCids {
            wasmCid: ipfs_hashes
                .get("wasmCid")
                .ok_or_else(|| eyre::eyre!("Missing wasmCid"))?
                .clone(),
            wasmArgs: ipfs_hashes.get("wasmArgs").unwrap_or(&String::new()).clone(),
            policyCid: ipfs_hashes
                .get("policyCid")
                .ok_or_else(|| eyre::eyre!("Missing policyCid"))?
                .clone(),
            schemaCid: ipfs_hashes
                .get("schemaCid")
                .ok_or_else(|| eyre::eyre!("Missing schemaCid"))?
                .clone(),
            attester: "0x4883282094755C01cd0d15dFE74753c9E189d194".to_string(),
            entrypoint: self.entrypoint.clone(),
            policyMetadataCid: ipfs_hashes
                .get("policyMetadataCid")
                .ok_or_else(|| eyre::eyre!("Missing policyMetadataCid"))?
                .clone(),
            policyDataMetadataCid: ipfs_hashes
                .get("policyDataMetadataCid")
                .ok_or_else(|| eyre::eyre!("Missing policyDataMetadataCid"))?
                .clone(),
        };

        // Write to JSON file
        let json_content =
            serde_json::to_string_pretty(&policy_cids).context("Failed to serialize policy_cids to JSON")?;

        // Create parent directory if it doesn't exist
        if let Some(parent) = self.output.parent() {
            std::fs::create_dir_all(parent).with_context(|| format!("Failed to create directory: {:?}", parent))?;
        }

        std::fs::write(&self.output, json_content)
            .with_context(|| format!("Failed to write to file: {:?}", self.output))?;

        tracing::info!("================================================");
        tracing::info!("========== Created policy_cids.json ============");
        tracing::info!("================================================");
        tracing::info!("Output file: {:?}", self.output);
        tracing::info!("");
        tracing::info!("{}", std::fs::read_to_string(&self.output)?);

        tracing::info!("Successfully created policy_cids.json");

        Ok(())
    }
}

impl PolicyFilesCommand {
    /// Execute the policy-files command
    pub async fn execute(self: Box<Self>, config: NewtonAvsConfig<NewtonCliConfig>) -> eyre::Result<()> {
        match self.subcommand {
            PolicyFilesSubcommand::GenerateCids(command) => {
                Box::new(command).execute(config).await?;
            }
        }
        Ok(())
    }
}
