use std::collections::BTreeMap;

use anyhow::{bail, Context, Result};
use atrium_api::com::atproto::sync::subscribe_repos::CommitData;
use bytes::Bytes;
use ecdsa::signature::Verifier;
use ipld_core::cid::Cid;
use iroh_car::CarReader;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::user::User;

#[derive(Debug, Serialize, Deserialize)]
pub struct UnsignedCommitNode {
    pub did: String,
    pub version: u8,
    pub prev: Option<Cid>,
    pub rev: String,
    pub data: Cid,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct SignedCommitNode {
    #[serde(flatten)]
    pub node: UnsignedCommitNode,
    pub sig: Bytes,
}

pub async fn validate_commit(user: &User, commit: &CommitData) -> Result<()> {
    let mut car_reader = CarReader::new(commit.blocks.as_slice()).await?;
    let car_header = car_reader.header().clone();

    let mut blocks = BTreeMap::new();
    while let Some((cid, cbor)) = car_reader.next_block().await? {
        let (hash_type, hash_digest, hash_len) = cid.hash().into_inner();
        if hash_type != 0x12 || hash_len != 32 {
            bail!(
                "unexpected cid type {:?} (expected sha-256 [id 18] with 32 bytes)",
                cid
            )
        }

        let cid_display = cid.to_string_of_base(multibase::Base::Base32Lower)?;

        let record_hash = Sha256::digest(&cbor);
        if &hash_digest[..32] != record_hash.as_slice() {
            bail!("cid doesn't match cbor hash: {}", &cid_display)
        }

        blocks.insert(cid, cbor);
    }

    /*
    let block_cids = blocks
        .keys()
        .filter_map(|cid| cid.to_string_of_base(multibase::Base::Base32Lower).ok())
        .collect::<Vec<_>>();
    dbg!(block_cids);
    let root_cids = car_header
        .roots()
        .iter()
        .filter_map(|cid| cid.to_string_of_base(multibase::Base::Base32Lower).ok())
        .collect::<Vec<_>>();
    dbg!(root_cids);
     */

    let signing_key = user
        .signing_key()
        .context("couldn't find signing key for user")?;
    let (_, signing_key) = multibase::decode(signing_key).unwrap();

    for root in car_header.roots() {
        let cid_display = root.to_string_of_base(multibase::Base::Base32Lower)?;

        let Some(block) = blocks.get(root) else {
            bail!("block did not exist: {}", &cid_display)
        };
        let commit: SignedCommitNode = serde_ipld_dagcbor::from_slice(block)
            .context("couldn't deserialize signed commit object")?;

        if commit.node.did != user.did {
            bail!("did in car doesn't match repo did {}", &cid_display)
        }

        let unsigned_data = serde_ipld_dagcbor::to_vec(&commit.node)?;
        if commit.sig.len() != 64 {
            bail!(
                "unexpected signature length {} (expected 64)",
                commit.sig.len()
            );
        }
        let r: [u8; 32] = commit.sig[..32].try_into().unwrap();
        let s: [u8; 32] = commit.sig[32..].try_into().unwrap();

        match signing_key[..2] {
            [0xe7, 0x01] => {
                let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&signing_key[2..]).unwrap();
                let sig = k256::ecdsa::Signature::from_scalars(r, s).unwrap();
                key.verify(&unsigned_data, &sig)
                    .context("failed to verify k256")
            }
            [0x80, 0x24] => {
                let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&signing_key[2..]).unwrap();
                let sig = p256::ecdsa::Signature::from_scalars(r, s).unwrap();
                key.verify(&unsigned_data, &sig)
                    .context("failed to verify p256")
            }
            _ => Err(anyhow::anyhow!(
                "unknown signing key format {:?}",
                &signing_key[..2]
            )),
        }?;

        // TODO: dfs for cid from commit.node.data, error if cid is not in any signed root
    }

    Ok(())
}