use serde::{Deserialize, Serialize};

use crate::address;
use crate::crypto;
use crate::error::BrowserCoreError;
use crate::{PRIVATE_KEY_LENGTH, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH};

const MESSAGE_DOMAIN: &[u8] = b"CONTRACTLESS_WEB_MESSAGE_V1\0";
const MAX_MESSAGE_BYTES: usize = 4096;
const MAX_ORIGIN_BYTES: usize = 255;
const MIN_NONCE_BYTES: usize = 16;
const MAX_PROOF_LIFETIME_SECONDS: u64 = 15 * 60;

#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessageRequest {
    pub origin: String,
    pub message: String,
    pub nonce: String,
    pub issued_at: u64,
    pub expires_at: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessagePayload {
    pub version: u8,
    pub origin: String,
    pub address: String,
    pub message: String,
    pub nonce: String,
    pub issued_at: u64,
    pub expires_at: u64,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessageProof {
    pub payload: MessagePayload,
    pub public_key: String,
    pub hash: String,
    pub signature: String,
}

#[derive(Debug, Serialize)]
pub struct MessageVerification {
    pub valid: bool,
    pub address: String,
    pub origin: String,
    pub message: String,
    pub nonce: String,
    pub issued_at: u64,
    pub expires_at: u64,
}

pub fn sign(
    request: MessageRequest,
    private_key: &[u8],
    public_key: &[u8],
) -> Result<MessageProof, BrowserCoreError> {
    if private_key.len() != PRIVATE_KEY_LENGTH {
        return Err(BrowserCoreError::InvalidPrivateKey);
    }
    if public_key.len() != PUBLIC_KEY_LENGTH {
        return Err(BrowserCoreError::InvalidPublicKey);
    }

    validate_request(&request)?;
    let payload = MessagePayload {
        version: 1,
        origin: request.origin,
        address: address::public_key_to_short_address(public_key)?,
        message: request.message,
        nonce: request.nonce.to_ascii_lowercase(),
        issued_at: request.issued_at,
        expires_at: request.expires_at,
    };
    let hash = payload_hash(&payload)?;
    let signature = crypto::sign(&hash, private_key)?;
    if !crypto::verify(&hash, &signature, public_key) {
        return Err(BrowserCoreError::InvalidRequest(
            "private key does not match the wallet public key".into(),
        ));
    }

    Ok(MessageProof {
        payload,
        public_key: hex::encode(public_key),
        hash: hex::encode(hash),
        signature: hex::encode(signature),
    })
}

pub fn verify(
    proof: MessageProof,
    expected_origin: &str,
    expected_nonce: &str,
    current_time: u64,
) -> Result<MessageVerification, BrowserCoreError> {
    if proof.payload.version != 1 {
        return Err(BrowserCoreError::InvalidRequest(
            "unsupported message proof version".into(),
        ));
    }
    validate_request(&MessageRequest {
        origin: proof.payload.origin.clone(),
        message: proof.payload.message.clone(),
        nonce: proof.payload.nonce.clone(),
        issued_at: proof.payload.issued_at,
        expires_at: proof.payload.expires_at,
    })?;
    let public_key =
        hex::decode(&proof.public_key).map_err(|_| BrowserCoreError::InvalidPublicKey)?;
    let derived_address = address::public_key_to_short_address(&public_key)?;
    if derived_address != proof.payload.address {
        return Err(BrowserCoreError::InvalidRequest(
            "message address does not match its public key".into(),
        ));
    }
    if proof.payload.origin != expected_origin {
        return Err(BrowserCoreError::InvalidRequest(
            "message proof was issued for a different origin".into(),
        ));
    }
    if !proof.payload.nonce.eq_ignore_ascii_case(expected_nonce) {
        return Err(BrowserCoreError::InvalidRequest(
            "message proof nonce does not match".into(),
        ));
    }
    if current_time < proof.payload.issued_at || current_time > proof.payload.expires_at {
        return Err(BrowserCoreError::InvalidRequest(
            "message proof is not currently valid".into(),
        ));
    }

    let hash = payload_hash(&proof.payload)?;
    if proof.hash != hex::encode(&hash) {
        return Err(BrowserCoreError::InvalidRequest(
            "message proof hash does not match its payload".into(),
        ));
    }
    let signature =
        hex::decode(&proof.signature).map_err(|_| BrowserCoreError::InvalidSignature)?;
    if signature.len() != SIGNATURE_LENGTH || !crypto::verify(&hash, &signature, &public_key) {
        return Err(BrowserCoreError::InvalidSignature);
    }

    Ok(MessageVerification {
        valid: true,
        address: proof.payload.address,
        origin: proof.payload.origin,
        message: proof.payload.message,
        nonce: proof.payload.nonce,
        issued_at: proof.payload.issued_at,
        expires_at: proof.payload.expires_at,
    })
}

fn validate_request(request: &MessageRequest) -> Result<(), BrowserCoreError> {
    let valid_origin = request
        .origin
        .strip_prefix("https://")
        .is_some_and(valid_https_authority)
        || request
            .origin
            .strip_prefix("http://localhost")
            .is_some_and(valid_local_port)
        || request
            .origin
            .strip_prefix("http://127.0.0.1")
            .is_some_and(valid_local_port);
    if request.origin.is_empty()
        || request.origin.len() > MAX_ORIGIN_BYTES
        || !request.origin.is_ascii()
        || request.origin.chars().any(char::is_whitespace)
        || !valid_origin
    {
        return Err(BrowserCoreError::InvalidRequest(
            "origin must be HTTPS or a local development origin".into(),
        ));
    }
    if request.message.is_empty() || request.message.as_bytes().len() > MAX_MESSAGE_BYTES {
        return Err(BrowserCoreError::InvalidRequest(format!(
            "message must contain 1 to {MAX_MESSAGE_BYTES} bytes"
        )));
    }
    let nonce = hex::decode(&request.nonce)
        .map_err(|_| BrowserCoreError::InvalidRequest("nonce must be hexadecimal".into()))?;
    if nonce.len() < MIN_NONCE_BYTES {
        return Err(BrowserCoreError::InvalidRequest(format!(
            "nonce must contain at least {MIN_NONCE_BYTES} random bytes"
        )));
    }
    if request.expires_at <= request.issued_at {
        return Err(BrowserCoreError::InvalidRequest(
            "expires_at must be after issued_at".into(),
        ));
    }
    if request.expires_at - request.issued_at > MAX_PROOF_LIFETIME_SECONDS {
        return Err(BrowserCoreError::InvalidRequest(format!(
            "message proofs may remain valid for at most {MAX_PROOF_LIFETIME_SECONDS} seconds"
        )));
    }
    Ok(())
}

fn valid_https_authority(authority: &str) -> bool {
    !authority.is_empty()
        && !authority.contains('/')
        && !authority.contains('?')
        && !authority.contains('#')
}

fn valid_local_port(suffix: &str) -> bool {
    suffix.is_empty()
        || suffix.strip_prefix(':').is_some_and(|port| {
            !port.is_empty() && port.chars().all(|character| character.is_ascii_digit())
        })
}

fn payload_hash(payload: &MessagePayload) -> Result<Vec<u8>, BrowserCoreError> {
    let json = serde_json::to_string(payload)
        .map_err(|error| BrowserCoreError::Json(error.to_string()))?;
    let mut domain_payload = Vec::with_capacity(MESSAGE_DOMAIN.len() + json.len());
    domain_payload.extend_from_slice(MESSAGE_DOMAIN);
    domain_payload.extend_from_slice(json.as_bytes());
    Ok(crypto::skein_256(&domain_payload))
}

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

    #[test]
    fn message_domain_differs_from_transaction_hashing() {
        let payload = MessagePayload {
            version: 1,
            origin: "https://example.com".into(),
            address: "11".repeat(20) + ".cltc",
            message: "Welcome to XYZ".into(),
            nonce: "22".repeat(16),
            issued_at: 100,
            expires_at: 200,
        };
        let json = serde_json::to_string(&payload).unwrap();
        assert_ne!(
            payload_hash(&payload).unwrap(),
            crypto::skein_256(json.as_bytes())
        );
    }

    #[test]
    fn message_requests_reject_long_lived_replay_windows() {
        let request = MessageRequest {
            origin: "https://example.com".into(),
            message: "Welcome to XYZ".into(),
            nonce: "22".repeat(16),
            issued_at: 100,
            expires_at: 100 + MAX_PROOF_LIFETIME_SECONDS + 1,
        };
        assert!(validate_request(&request).is_err());
    }
}
