//! Type definitions for tap events and API responses.

use serde::{Deserialize, Serialize};

/// A record's JSON data with helper methods.
///
/// Wraps the raw JSON value and provides convenient accessors.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Record(serde_json::Value);

impl Record {
    /// Create a new Record from a JSON value.
    pub fn new(value: serde_json::Value) -> Self {
        Self(value)
    }

    /// Access the raw JSON value.
    pub fn json(&self) -> &serde_json::Value {
        &self.0
    }

    /// Consume and return the inner JSON value.
    pub fn into_json(self) -> serde_json::Value {
        self.0
    }

    /// Returns the `$type` field (e.g., "app.bsky.feed.post").
    pub fn record_type(&self) -> Option<&str> {
        self.0.get("$type")?.as_str()
    }

    /// Deserialize the record into a user-provided type.
    pub fn deserialize_as<T: serde::de::DeserializeOwned>(&self) -> crate::Result<T> {
        Ok(serde_json::from_value(self.0.clone())?)
    }
}

/// Action performed on a record.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum RecordAction {
    Create,
    Update,
    Delete,
}

/// Account status.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum AccountStatus {
    Active,
    Takendown,
    Suspended,
    Deactivated,
    Deleted,
}

/// Repository sync state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum RepoState {
    Pending,
    Desynchronized,
    Resyncing,
    Active,
    Takendown,
    Suspended,
    Deactivated,
    Error,
}

/// A record event from the tap stream.
#[derive(Debug, Clone)]
pub struct RecordEvent {
    /// Unique event ID for acknowledgment.
    pub id: u64,
    /// True if from live firehose, false if from backfill/resync.
    pub live: bool,
    /// DID of the repository.
    pub did: String,
    /// Repository revision (TID format).
    pub rev: String,
    /// Collection NSID (e.g., "app.bsky.feed.post").
    pub collection: String,
    /// Record key (usually TID format).
    pub rkey: String,
    /// Action performed on the record.
    pub action: RecordAction,
    /// CID of the record (None on delete).
    pub cid: Option<String>,
    /// The record data (None on delete).
    pub record: Option<Record>,
}

/// An identity event from the tap stream.
#[derive(Debug, Clone)]
pub struct IdentityEvent {
    /// Unique event ID for acknowledgment.
    pub id: u64,
    /// DID of the account.
    pub did: String,
    /// Current handle.
    pub handle: String,
    /// Whether the account is active.
    pub is_active: bool,
    /// Account status.
    pub status: AccountStatus,
}

/// An event from the tap stream.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Event {
    /// A record create/update/delete event.
    Record(RecordEvent),
    /// An identity (handle/status) change event.
    Identity(IdentityEvent),
}

impl Event {
    /// Get the event ID.
    pub fn id(&self) -> u64 {
        match self {
            Event::Record(e) => e.id,
            Event::Identity(e) => e.id,
        }
    }

    /// Get the DID associated with this event.
    pub fn did(&self) -> &str {
        match self {
            Event::Record(e) => &e.did,
            Event::Identity(e) => &e.did,
        }
    }
}

/// Information about a tracked repository.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoInfo {
    /// DID of the repository.
    pub did: String,
    /// Current handle (may be empty).
    pub handle: String,
    /// Sync state.
    pub state: RepoState,
    /// Current revision (TID format, empty if not synced).
    pub rev: String,
    /// Error message if in error state.
    pub error: String,
    /// Number of failed retry attempts.
    pub retries: u32,
    /// Total number of tracked records.
    pub records: u64,
}

/// Cursor positions.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Cursors {
    /// Firehose sequence number (None if not consuming).
    pub firehose: Option<i64>,
    /// List repos enumeration cursor (None if not enumerating).
    pub list_repos: Option<String>,
}

/// A DID document.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DidDocument {
    /// The DID itself.
    pub id: String,
    /// Also known as (ATProto handles, etc.).
    #[serde(default, rename = "alsoKnownAs")]
    pub also_known_as: Vec<String>,
    /// Verification methods (signing keys).
    #[serde(default, rename = "verificationMethod")]
    pub verification_method: Vec<VerificationMethod>,
    /// Services (PDS endpoint, etc.).
    #[serde(default)]
    pub service: Vec<Service>,
    /// Additional fields not explicitly modelled.
    #[serde(flatten)]
    pub extra: serde_json::Value,
}

/// A verification method in a DID document.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationMethod {
    /// Method ID.
    pub id: String,
    /// Method type.
    #[serde(rename = "type")]
    pub type_: String,
    /// Controller DID.
    pub controller: String,
    /// Public key in multibase format.
    #[serde(rename = "publicKeyMultibase")]
    pub public_key_multibase: Option<String>,
}

/// A service in a DID document.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
    /// Service ID.
    pub id: String,
    /// Service type.
    #[serde(rename = "type")]
    pub type_: String,
    /// Service endpoint URL.
    #[serde(rename = "serviceEndpoint")]
    pub service_endpoint: String,
}

// Internal deserialisation structures for parsing tap's JSON format

#[derive(Deserialize)]
pub(crate) struct RawEvent {
    pub id: u64,
    #[serde(rename = "type")]
    pub type_: String,
    pub record: Option<RawRecordEvent>,
    pub identity: Option<RawIdentityEvent>,
}

#[derive(Deserialize)]
pub(crate) struct RawRecordEvent {
    pub live: bool,
    pub did: String,
    pub rev: String,
    pub collection: String,
    pub rkey: String,
    pub action: RecordAction,
    pub cid: Option<String>,
    pub record: Option<serde_json::Value>,
}

#[derive(Deserialize)]
pub(crate) struct RawIdentityEvent {
    pub did: String,
    pub handle: String,
    #[serde(rename = "is_active")]
    pub is_active: bool,
    pub status: AccountStatus,
}

impl RawEvent {
    /// Convert to the public Event type.
    pub fn into_event(self) -> Option<Event> {
        match self.type_.as_str() {
            "record" => {
                let r = self.record?;
                Some(Event::Record(RecordEvent {
                    id: self.id,
                    live: r.live,
                    did: r.did,
                    rev: r.rev,
                    collection: r.collection,
                    rkey: r.rkey,
                    action: r.action,
                    cid: r.cid,
                    record: r.record.map(Record::new),
                }))
            }
            "identity" => {
                let i = self.identity?;
                Some(Event::Identity(IdentityEvent {
                    id: self.id,
                    did: i.did,
                    handle: i.handle,
                    is_active: i.is_active,
                    status: i.status,
                }))
            }
            _ => None,
        }
    }
}

// Response types for stats endpoints

#[derive(Deserialize)]
pub(crate) struct RepoCountResponse {
    pub repo_count: u64,
}

#[derive(Deserialize)]
pub(crate) struct RecordCountResponse {
    pub record_count: u64,
}

#[derive(Deserialize)]
pub(crate) struct OutboxBufferResponse {
    pub outbox_buffer: u64,
}

#[derive(Deserialize)]
pub(crate) struct ResyncBufferResponse {
    pub resync_buffer: u64,
}

#[derive(Deserialize)]
pub(crate) struct ApiError {
    pub message: String,
}

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

    #[test]
    fn record_type_extraction() {
        let record = Record::new(json!({
            "$type": "app.bsky.feed.post",
            "text": "Hello, world!",
            "createdAt": "2024-01-01T00:00:00Z"
        }));

        assert_eq!(record.record_type(), Some("app.bsky.feed.post"));
    }

    #[test]
    fn record_type_missing() {
        let record = Record::new(json!({
            "text": "No type field"
        }));

        assert_eq!(record.record_type(), None);
    }

    #[test]
    fn record_deserialize_as() {
        #[derive(Debug, Deserialize, PartialEq)]
        struct SimpleRecord {
            text: String,
        }

        let record = Record::new(json!({
            "$type": "test.record",
            "text": "Hello!"
        }));

        let parsed: SimpleRecord = record.deserialize_as().unwrap();
        assert_eq!(parsed.text, "Hello!");
    }

    /// Helper macro for testing enum variant deserialisation.
    macro_rules! assert_deserialize {
        ($type:ty, $($json:literal => $variant:expr),+ $(,)?) => {
            $(
                assert_eq!(
                    serde_json::from_str::<$type>($json).unwrap(),
                    $variant
                );
            )+
        };
    }

    #[test]
    fn record_action_deserialize() {
        assert_deserialize!(RecordAction,
            r#""create""# => RecordAction::Create,
            r#""update""# => RecordAction::Update,
            r#""delete""# => RecordAction::Delete,
        );
    }

    #[test]
    fn account_status_deserialize() {
        assert_deserialize!(AccountStatus,
            r#""active""# => AccountStatus::Active,
            r#""takendown""# => AccountStatus::Takendown,
            r#""suspended""# => AccountStatus::Suspended,
            r#""deactivated""# => AccountStatus::Deactivated,
            r#""deleted""# => AccountStatus::Deleted,
        );
    }

    #[test]
    fn repo_state_deserialize() {
        assert_deserialize!(RepoState,
            r#""pending""# => RepoState::Pending,
            r#""active""# => RepoState::Active,
            r#""error""# => RepoState::Error,
        );
    }

    #[test]
    fn repo_info_deserialize() {
        let json = json!({
            "did": "did:plc:abc123",
            "handle": "test.bsky.social",
            "state": "active",
            "rev": "3abc123",
            "error": "",
            "retries": 0,
            "records": 42
        });

        let info: RepoInfo = serde_json::from_value(json).unwrap();
        assert_eq!(info.did, "did:plc:abc123");
        assert_eq!(info.handle, "test.bsky.social");
        assert_eq!(info.state, RepoState::Active);
        assert_eq!(info.records, 42);
    }

    #[test]
    fn cursors_deserialize() {
        let json = json!({
            "firehose": 12345678,
            "list_repos": "some-cursor"
        });

        let cursors: Cursors = serde_json::from_value(json).unwrap();
        assert_eq!(cursors.firehose, Some(12345678));
        assert_eq!(cursors.list_repos, Some("some-cursor".to_string()));
    }

    #[test]
    fn cursors_deserialize_nulls() {
        let json = json!({
            "firehose": null,
            "list_repos": null
        });

        let cursors: Cursors = serde_json::from_value(json).unwrap();
        assert_eq!(cursors.firehose, None);
        assert_eq!(cursors.list_repos, None);
    }

    #[test]
    fn did_document_deserialize() {
        let json = json!({
            "id": "did:plc:example1234567890abc",
            "alsoKnownAs": ["at://alice.test"],
            "verificationMethod": [{
                "id": "did:plc:example1234567890abc#atproto",
                "type": "Multikey",
                "controller": "did:plc:example1234567890abc",
                "publicKeyMultibase": "zDnaekeGCpVsdvDCrGNa9t3bXYUs45MHX1hLwqvaKLtPU9m7X"
            }],
            "service": [{
                "id": "#atproto_pds",
                "type": "AtprotoPersonalDataServer",
                "serviceEndpoint": "https://pds.example.com"
            }]
        });

        let doc: DidDocument = serde_json::from_value(json).unwrap();
        assert_eq!(doc.id, "did:plc:example1234567890abc");
        assert_eq!(doc.also_known_as, vec!["at://alice.test"]);
        assert_eq!(doc.verification_method.len(), 1);
        assert_eq!(doc.verification_method[0].type_, "Multikey");
        assert_eq!(doc.service.len(), 1);
        assert_eq!(doc.service[0].type_, "AtprotoPersonalDataServer");
    }

    #[test]
    fn did_document_with_extra_fields() {
        let json = json!({
            "id": "did:plc:test",
            "alsoKnownAs": [],
            "@context": ["https://www.w3.org/ns/did/v1"],
            "customField": "some value"
        });

        let doc: DidDocument = serde_json::from_value(json).unwrap();
        assert_eq!(doc.id, "did:plc:test");
        assert!(doc.extra.get("@context").is_some());
        assert!(doc.extra.get("customField").is_some());
    }

    #[test]
    fn raw_record_event_deserialize() {
        let json = json!({
            "id": 12345,
            "type": "record",
            "record": {
                "live": true,
                "did": "did:plc:abc123",
                "rev": "3abc",
                "collection": "app.bsky.feed.post",
                "rkey": "3def",
                "action": "create",
                "cid": "bafyreid...",
                "record": {
                    "$type": "app.bsky.feed.post",
                    "text": "Hello!"
                }
            }
        });

        let raw: RawEvent = serde_json::from_value(json).unwrap();
        assert_eq!(raw.id, 12345);
        assert_eq!(raw.type_, "record");

        let event = raw.into_event().unwrap();
        match event {
            Event::Record(r) => {
                assert_eq!(r.id, 12345);
                assert!(r.live);
                assert_eq!(r.did, "did:plc:abc123");
                assert_eq!(r.collection, "app.bsky.feed.post");
                assert_eq!(r.action, RecordAction::Create);
                assert!(r.record.is_some());
                assert_eq!(r.record.unwrap().record_type(), Some("app.bsky.feed.post"));
            }
            _ => panic!("Expected Record event"),
        }
    }

    #[test]
    fn raw_identity_event_deserialize() {
        let json = json!({
            "id": 99999,
            "type": "identity",
            "identity": {
                "did": "did:plc:xyz789",
                "handle": "alice.bsky.social",
                "is_active": true,
                "status": "active"
            }
        });

        let raw: RawEvent = serde_json::from_value(json).unwrap();
        let event = raw.into_event().unwrap();

        match event {
            Event::Identity(i) => {
                assert_eq!(i.id, 99999);
                assert_eq!(i.did, "did:plc:xyz789");
                assert_eq!(i.handle, "alice.bsky.social");
                assert!(i.is_active);
                assert_eq!(i.status, AccountStatus::Active);
            }
            _ => panic!("Expected Identity event"),
        }
    }

    #[test]
    fn raw_delete_event_no_record() {
        let json = json!({
            "id": 55555,
            "type": "record",
            "record": {
                "live": false,
                "did": "did:plc:deleted",
                "rev": "3xyz",
                "collection": "app.bsky.feed.post",
                "rkey": "3abc",
                "action": "delete",
                "cid": null,
                "record": null
            }
        });

        let raw: RawEvent = serde_json::from_value(json).unwrap();
        let event = raw.into_event().unwrap();

        match event {
            Event::Record(r) => {
                assert_eq!(r.action, RecordAction::Delete);
                assert!(r.cid.is_none());
                assert!(r.record.is_none());
            }
            _ => panic!("Expected Record event"),
        }
    }

    #[test]
    fn event_helper_methods() {
        let record_event = Event::Record(RecordEvent {
            id: 123,
            live: true,
            did: "did:plc:record".to_string(),
            rev: "abc".to_string(),
            collection: "test".to_string(),
            rkey: "key".to_string(),
            action: RecordAction::Create,
            cid: None,
            record: None,
        });

        assert_eq!(record_event.id(), 123);
        assert_eq!(record_event.did(), "did:plc:record");

        let identity_event = Event::Identity(IdentityEvent {
            id: 456,
            did: "did:plc:identity".to_string(),
            handle: "test".to_string(),
            is_active: true,
            status: AccountStatus::Active,
        });

        assert_eq!(identity_event.id(), 456);
        assert_eq!(identity_event.did(), "did:plc:identity");
    }
}
