//! Oracle key material — generation, persistence, and (crate-private) signing.
//!
//! [`OracleIdentity`] is the only place in this crate where an `ed25519`
//! signing key lives. The public surface is intentionally narrow:
//!
//! - [`OracleIdentity::generate`] — create a fresh keypair from `OsRng`.
//! - [`OracleIdentity::from_secret_bytes`] — rehydrate from raw 32 secret bytes.
//! - [`OracleIdentity::load_from_file`] / [`OracleIdentity::save_to_file`] —
//!   JSON keystore matching `fialucci-oracle-keygen`'s on-disk format
//!   (`oracle_id` / `public_key_hex` / `private_key_hex`).
//! - [`OracleIdentity::oracle_id`] and [`OracleIdentity::public_key`] —
//!   read-only accessors.
//!
//! Notably absent from the public surface: any method that signs arbitrary
//! bytes. The runtime calls [`OracleIdentity::sign_body`] (crate-private) to
//! produce a [`SignedOracleDataSubmission`]; nothing else can. This is the
//! mechanism by which "devs can't change how the oracle interacts with the
//! network" stays enforced — the dev never touches a signing primitive.
//!
//! ## Keystore format
//!
//! Identical to the chain's `fialucci-oracle-keygen` output so a key
//! generated by either tool works with the other:
//!
//! ```json
//! {
//!   "oracle_id":      "<64 lowercase hex chars — SHA3-256(public_key_bytes)>",
//!   "public_key_hex": "<64 lowercase hex chars — ed25519 public key>",
//!   "private_key_hex":"<64 lowercase hex chars — ed25519 secret seed>"
//! }
//! ```

use std::fs;
use std::path::Path;

use ed25519_dalek::SigningKey;
use fialucci_types::{
    derive_oracle_id, sign_oracle_data_submission, OracleDataSubmissionBody,
    OracleDataSubmissionSigningError, OracleId, OraclePublicKeyBytes, OracleSigningKey,
    SignedOracleDataSubmission,
};
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};

/// An oracle's ed25519 keypair plus its derived [`OracleId`].
///
/// Identities are created with [`generate`](Self::generate), then either kept
/// in memory or persisted via [`save_to_file`](Self::save_to_file) and
/// reloaded later via [`load_from_file`](Self::load_from_file).
///
/// The signing key is not exposed; the only way to sign anything is to hand
/// this identity to an [`Oracle`](crate::Oracle) and let the runtime do it.
pub struct OracleIdentity {
    signing_key: OracleSigningKey,
    public_key: OraclePublicKeyBytes,
    oracle_id: OracleId,
}

impl OracleIdentity {
    /// Generate a fresh random oracle identity using `OsRng`.
    pub fn generate() -> Self {
        let sk = SigningKey::generate(&mut OsRng);
        Self::from_signing_key(sk)
    }

    /// Rehydrate an identity from raw 32 secret seed bytes (e.g. read from a
    /// hardware-stored backup). The matching public key and `OracleId` are
    /// recomputed.
    pub fn from_secret_bytes(secret: &[u8; 32]) -> Self {
        let sk = SigningKey::from_bytes(secret);
        Self::from_signing_key(sk)
    }

    fn from_signing_key(sk: SigningKey) -> Self {
        let public_key = OraclePublicKeyBytes(sk.verifying_key().to_bytes());
        let oracle_id = derive_oracle_id(&public_key);
        Self {
            signing_key: OracleSigningKey(sk),
            public_key,
            oracle_id,
        }
    }

    /// This oracle's deterministic, public-key-derived identifier
    /// (`SHA3-256(public_key_bytes)`).
    #[inline]
    pub fn oracle_id(&self) -> OracleId {
        self.oracle_id
    }

    /// The ed25519 public-key bytes (32 bytes).
    #[inline]
    pub fn public_key(&self) -> OraclePublicKeyBytes {
        self.public_key.clone()
    }

    /// Load an identity from the keystore-JSON file written by
    /// [`save_to_file`](Self::save_to_file) or by the chain's
    /// `fialucci-oracle-keygen` binary.
    ///
    /// The file's `oracle_id` and `public_key_hex` fields are verified to
    /// match the values derived from `private_key_hex`; a mismatch returns
    /// [`IdentityError::KeystoreMismatch`].
    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, IdentityError> {
        let path = path.as_ref();
        let bytes = fs::read(path).map_err(|e| IdentityError::Io {
            path: path.display().to_string(),
            source: e,
        })?;
        let file: KeystoreFile = serde_json::from_slice(&bytes)?;

        let secret = decode_hex_32(&file.private_key_hex, "private_key_hex")?;
        let identity = Self::from_secret_bytes(&secret);

        // Cross-check: the file's claimed pubkey + oracle_id must match what
        // we just rederived from the secret. Catches accidental edits.
        let expected_public = decode_hex_32(&file.public_key_hex, "public_key_hex")?;
        if expected_public != identity.public_key.0 {
            return Err(IdentityError::KeystoreMismatch {
                field: "public_key_hex",
            });
        }
        let expected_id = decode_hex_32(&file.oracle_id, "oracle_id")?;
        if expected_id != identity.oracle_id.0 {
            return Err(IdentityError::KeystoreMismatch { field: "oracle_id" });
        }

        Ok(identity)
    }

    /// Persist this identity to disk as a JSON keystore matching
    /// `fialucci-oracle-keygen`'s output format.
    ///
    /// Best-effort tightens file permissions to `0600` on Unix after the
    /// write; on other platforms the permissions are whatever the OS
    /// defaults to.
    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), IdentityError> {
        let path = path.as_ref();
        let file = KeystoreFile {
            oracle_id: hex::encode(self.oracle_id.0),
            public_key_hex: hex::encode(self.public_key.0),
            private_key_hex: hex::encode(self.signing_key.0.to_bytes()),
        };
        let json = serde_json::to_vec_pretty(&file)?;
        fs::write(path, json).map_err(|e| IdentityError::Io {
            path: path.display().to_string(),
            source: e,
        })?;

        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mut perms = fs::metadata(path)
                .map_err(|e| IdentityError::Io {
                    path: path.display().to_string(),
                    source: e,
                })?
                .permissions();
            perms.set_mode(0o600);
            // Best-effort: a chmod failure shouldn't fail the save itself.
            let _ = fs::set_permissions(path, perms);
        }

        Ok(())
    }

    /// Sign an [`OracleDataSubmissionBody`] into a fully-formed
    /// [`SignedOracleDataSubmission`], via `fialucci-types`'s authoritative
    /// `sign_oracle_data_submission`.
    ///
    /// Crate-private: only the SDK's runtime constructs bodies, so only it
    /// signs them. The dev's [`OracleHandler`](crate::OracleHandler)
    /// contributes the claim and evidence, never the signing call.
    pub(crate) fn sign_body(
        &self,
        body: OracleDataSubmissionBody,
    ) -> Result<SignedOracleDataSubmission, OracleDataSubmissionSigningError> {
        sign_oracle_data_submission(body, &self.signing_key)
    }
}

impl std::fmt::Debug for OracleIdentity {
    /// Redacts the signing key so it cannot leak via logs or trace output.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("OracleIdentity")
            .field("oracle_id", &hex::encode(self.oracle_id.0))
            .field("public_key", &hex::encode(self.public_key.0))
            .field("signing_key", &"<redacted>")
            .finish()
    }
}

#[derive(Serialize, Deserialize)]
struct KeystoreFile {
    oracle_id: String,
    public_key_hex: String,
    private_key_hex: String,
}

fn decode_hex_32(hex_str: &str, field: &'static str) -> Result<[u8; 32], IdentityError> {
    let bytes = hex::decode(hex_str).map_err(|e| IdentityError::HexDecode { field, source: e })?;
    if bytes.len() != 32 {
        return Err(IdentityError::WrongLength {
            field,
            expected: 32,
            got: bytes.len(),
        });
    }
    let mut out = [0u8; 32];
    out.copy_from_slice(&bytes);
    Ok(out)
}

/// Errors surfaced by [`OracleIdentity`] keystore operations.
#[derive(Debug, thiserror::Error)]
pub enum IdentityError {
    /// File-system read/write failure.
    #[error("I/O error on {path}: {source}")]
    Io {
        /// Path being read or written when the error occurred.
        path: String,
        /// Underlying [`std::io::Error`].
        source: std::io::Error,
    },
    /// The keystore file's JSON could not be parsed.
    #[error("keystore JSON parse error: {0}")]
    Json(#[from] serde_json::Error),
    /// One of the keystore's hex-encoded fields failed `hex::decode`.
    #[error("hex decode error in field `{field}`: {source}")]
    HexDecode {
        /// Keystore field name (`oracle_id`, `public_key_hex`, or `private_key_hex`).
        field: &'static str,
        /// Underlying [`hex::FromHexError`].
        source: hex::FromHexError,
    },
    /// A hex-decoded keystore field had an unexpected byte length.
    #[error("field `{field}` expected {expected} bytes, got {got}")]
    WrongLength {
        /// Keystore field name.
        field: &'static str,
        /// Required byte length.
        expected: usize,
        /// Observed byte length.
        got: usize,
    },
    /// The keystore file's `public_key_hex` or `oracle_id` does not match
    /// what the SDK rederived from the loaded `private_key_hex`. Typically
    /// caused by hand-editing the file.
    #[error("keystore field `{field}` does not match value derived from private key")]
    KeystoreMismatch {
        /// Keystore field whose value diverged from the derived one.
        field: &'static str,
    },
}

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

    #[test]
    fn generated_identity_round_trips_through_keystore() {
        let original = OracleIdentity::generate();
        let original_id = original.oracle_id();
        let original_pk = original.public_key();

        let tmp = NamedTempFile::new().unwrap();
        original.save_to_file(tmp.path()).unwrap();

        let loaded = OracleIdentity::load_from_file(tmp.path()).unwrap();
        assert_eq!(loaded.oracle_id(), original_id);
        assert_eq!(loaded.public_key().0, original_pk.0);
    }

    #[test]
    fn oracle_id_equals_sha3_of_pubkey() {
        // Sanity: the SDK's derived id matches `fialucci-types::derive_oracle_id`
        // exactly, since that's literally what `from_signing_key` calls.
        let id = OracleIdentity::generate();
        let derived = derive_oracle_id(&id.public_key());
        assert_eq!(id.oracle_id(), derived);
    }

    #[test]
    fn keystore_mismatch_detects_edited_pubkey() {
        let id = OracleIdentity::generate();
        let tmp = NamedTempFile::new().unwrap();
        id.save_to_file(tmp.path()).unwrap();

        // Corrupt the public_key_hex by flipping one nibble.
        let raw = std::fs::read_to_string(tmp.path()).unwrap();
        let mut file: KeystoreFile = serde_json::from_str(&raw).unwrap();
        let mut chars: Vec<char> = file.public_key_hex.chars().collect();
        chars[0] = if chars[0] == '0' { '1' } else { '0' };
        file.public_key_hex = chars.into_iter().collect();
        std::fs::write(tmp.path(), serde_json::to_vec(&file).unwrap()).unwrap();

        match OracleIdentity::load_from_file(tmp.path()) {
            Err(IdentityError::KeystoreMismatch {
                field: "public_key_hex",
            }) => {}
            other => panic!("expected KeystoreMismatch, got {:?}", other.is_ok()),
        }
    }

    #[test]
    fn debug_redacts_signing_key() {
        let id = OracleIdentity::generate();
        let s = format!("{:?}", id);
        assert!(
            s.contains("<redacted>"),
            "Debug must redact signing_key, got: {s}"
        );
    }
}
