use anyhow::{anyhow, Context, Result};
use ec_core::Manifest;
use ec_eth::{
    b256_hex, keccak256_bytes_hex, manifest_observation_header, observation_header_hash,
    observation_slot_hash,
};
use serde::Serialize;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use tokio::process::Command;

pub const OBSERVATION_RPC_URL_ENV: &str = "EVERY_CHANNEL_OBSERVATION_RPC_URL";
pub const OBSERVATION_LEDGER_ENV: &str = "EVERY_CHANNEL_OBSERVATION_LEDGER";
pub const OBSERVATION_PRIVATE_KEY_ENV: &str = "EVERY_CHANNEL_OBSERVATION_PRIVATE_KEY";
pub const OBSERVATION_PRIVATE_KEY_FILE_ENV: &str = "EVERY_CHANNEL_OBSERVATION_PRIVATE_KEY_FILE";
pub const OBSERVATION_PARENT_HASH_ENV: &str = "EVERY_CHANNEL_OBSERVATION_PARENT_HASH";

#[derive(Debug, Clone)]
pub struct ObservationSinkOptions {
    pub rpc_url: Option<String>,
    pub ledger: Option<String>,
    pub private_key: Option<String>,
    pub private_key_file: Option<PathBuf>,
    pub parent_hash: Option<String>,
    pub timeout_ms: u64,
}

#[derive(Debug, Clone)]
pub struct ObservationSink {
    rpc_url: String,
    ledger: String,
    private_key: String,
    parent_hash: Option<String>,
    timeout: Duration,
}

#[derive(Debug, Clone, Serialize)]
pub struct ManifestObservationLocator {
    pub transport: String,
    pub broadcast_name: String,
    pub track_name: String,
    pub manifest_track: String,
    pub stream_id: String,
    pub epoch_id: String,
    pub manifest_id: String,
}

#[derive(Debug, Clone)]
pub struct SubmittedObservation {
    pub observation_hash: String,
    pub slot_hash: String,
    pub stream_hash: String,
    pub epoch_hash: String,
    pub data_root: String,
    pub locator_hash: String,
    pub sequence: u64,
}

fn env_value(name: &str) -> Option<String> {
    std::env::var(name)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn normalize_private_key(value: &str) -> Result<String> {
    let trimmed = value.trim().strip_prefix("0x").unwrap_or(value.trim());
    let bytes = hex::decode(trimmed).context("observation private key must be hex")?;
    if bytes.len() != 32 {
        return Err(anyhow!("observation private key must be 32 bytes"));
    }
    Ok(format!("0x{}", hex::encode(bytes)))
}

fn read_private_key_file(path: &PathBuf) -> Result<String> {
    fs::read_to_string(path)
        .with_context(|| format!("failed to read observation private key {}", path.display()))
        .and_then(|value| normalize_private_key(&value))
}

impl ObservationSink {
    pub fn from_options(options: ObservationSinkOptions) -> Result<Option<Self>> {
        let rpc_url = options
            .rpc_url
            .or_else(|| env_value(OBSERVATION_RPC_URL_ENV));
        let ledger = options.ledger.or_else(|| env_value(OBSERVATION_LEDGER_ENV));
        let private_key = options
            .private_key
            .or_else(|| env_value(OBSERVATION_PRIVATE_KEY_ENV));
        let private_key_file = options
            .private_key_file
            .or_else(|| env_value(OBSERVATION_PRIVATE_KEY_FILE_ENV).map(PathBuf::from));
        let parent_hash = options
            .parent_hash
            .or_else(|| env_value(OBSERVATION_PARENT_HASH_ENV));

        if rpc_url.is_none()
            && ledger.is_none()
            && private_key.is_none()
            && private_key_file.is_none()
        {
            return Ok(None);
        }

        let rpc_url = rpc_url.ok_or_else(|| {
            anyhow!(
                "set --observation-rpc-url or {} for observation submission",
                OBSERVATION_RPC_URL_ENV
            )
        })?;
        let ledger = ledger.ok_or_else(|| {
            anyhow!(
                "set --observation-ledger or {} for observation submission",
                OBSERVATION_LEDGER_ENV
            )
        })?;
        let private_key = match (private_key, private_key_file) {
            (Some(value), _) => normalize_private_key(&value)?,
            (None, Some(path)) => read_private_key_file(&path)?,
            (None, None) => {
                return Err(anyhow!(
                    "set --observation-private-key, --observation-private-key-file, {}, or {}",
                    OBSERVATION_PRIVATE_KEY_ENV,
                    OBSERVATION_PRIVATE_KEY_FILE_ENV
                ))
            }
        };

        Ok(Some(Self {
            rpc_url,
            ledger,
            private_key,
            parent_hash,
            timeout: Duration::from_millis(options.timeout_ms.max(1_000)),
        }))
    }

    pub async fn submit_manifest(
        &self,
        manifest: &Manifest,
        locator: ManifestObservationLocator,
    ) -> Result<SubmittedObservation> {
        let locator_json =
            serde_json::to_string(&locator).context("failed to encode observation locator")?;
        let locator_hash = keccak256_bytes_hex(locator_json.as_bytes());
        let sequence = manifest.body.chunk_start_index;
        let header = manifest_observation_header(
            manifest,
            self.parent_hash.as_deref(),
            &locator_hash,
            sequence,
        )
        .map_err(|err| anyhow!(err))?;

        let stream_hash = b256_hex(header.streamHash);
        let epoch_hash = b256_hex(header.epochHash);
        let data_root = b256_hex(header.dataRoot);
        let observation_hash = observation_header_hash(&header);
        let slot_hash =
            observation_slot_hash(&stream_hash, &epoch_hash).map_err(|err| anyhow!(err))?;
        let tuple = format!(
            "({},{},{},{},{},{},{})",
            stream_hash,
            epoch_hash,
            b256_hex(header.parentObservationHash),
            data_root,
            locator_hash,
            header.observedUnixMs,
            header.sequence
        );

        let mut cmd = Command::new("cast");
        cmd.arg("send")
            .arg(&self.ledger)
            .arg("proposeObservation((bytes32,bytes32,bytes32,bytes32,bytes32,uint64,uint64))")
            .arg(&tuple)
            .arg("--rpc-url")
            .arg(&self.rpc_url)
            .arg("--private-key")
            .arg(&self.private_key);

        let output = tokio::time::timeout(self.timeout, cmd.output())
            .await
            .context("timed out submitting observation")?
            .context("failed to run cast for observation submission")?;
        if !output.status.success() {
            return Err(anyhow!(
                "cast observation submission failed: {}",
                String::from_utf8_lossy(&output.stderr).trim()
            ));
        }

        Ok(SubmittedObservation {
            observation_hash,
            slot_hash,
            stream_hash,
            epoch_hash,
            data_root,
            locator_hash,
            sequence,
        })
    }
}

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

    #[test]
    fn normalize_private_key_accepts_prefixed_and_plain_hex() {
        let plain = "11".repeat(32);
        assert_eq!(normalize_private_key(&plain).unwrap(), format!("0x{plain}"));
        assert_eq!(
            normalize_private_key(&format!("0x{plain}")).unwrap(),
            format!("0x{plain}")
        );
        assert!(normalize_private_key("abcd").is_err());
    }
}
