use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};

use crate::BillingSubjectRef;
use crate::canonical::{
    CanonicalBillingDocument, CanonicalInvoice, CanonicalPayment, CanonicalSubscription,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BillingGrant {
    pub key: &'static str,
    pub resource: &'static str,
    pub action: &'static str,
    pub description: &'static str,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BillingGrantSourceKind {
    Payment,
    Subscription,
    Invoice,
}

impl BillingGrantSourceKind {
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Payment => "payment",
            Self::Subscription => "subscription",
            Self::Invoice => "invoice",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingGrantProjection {
    pub source_kind: BillingGrantSourceKind,
    pub source_ref: String,
    #[serde(
        default,
        rename = "rightModules",
        alias = "grantModules",
        alias = "nativeRightModules"
    )]
    pub right_modules: Vec<BillingRightModuleInput>,
    pub right_keys: Vec<String>,
    pub grant_keys: Vec<String>,
    pub permissions: Value,
    pub metadata: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingAuthGrantSyncRequest {
    pub subject_kind: String,
    pub subject_id: String,
    pub source_kind: String,
    pub source_ref: String,
    #[serde(
        default,
        rename = "rightModules",
        alias = "grantModules",
        alias = "nativeRightModules"
    )]
    pub right_modules: Vec<BillingRightModuleInput>,
    pub right_keys: Vec<String>,
    pub grant_keys: Vec<String>,
    pub metadata: Value,
}

pub type BillingRight = BillingGrant;
pub type BillingRightProjection = BillingGrantProjection;
pub type BillingAuthRightSyncRequest = BillingAuthGrantSyncRequest;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingRightModuleInput {
    pub key: String,
    #[serde(default, rename = "rightKeys", alias = "grantKeys")]
    pub right_keys: Vec<String>,
}

pub const BILLING_READ_GRANT: BillingGrant = BillingGrant {
    key: "billing.read",
    resource: "billing",
    action: "read",
    description: "Read canonical billing records and provider-backed billing state.",
};

pub const BILLING_MANAGE_GRANT: BillingGrant = BillingGrant {
    key: "billing.manage",
    resource: "billing",
    action: "manage",
    description: "Create or update billing resources and orchestration state.",
};

pub const BILLING_CUSTOMER_MANAGE_GRANT: BillingGrant = BillingGrant {
    key: "billing.customer.manage",
    resource: "billing.customer",
    action: "manage",
    description: "Manage billing customer bindings and customer lifecycle state.",
};

pub const BILLING_SUBSCRIPTION_MANAGE_GRANT: BillingGrant = BillingGrant {
    key: "billing.subscription.manage",
    resource: "billing.subscription",
    action: "manage",
    description: "Manage subscription lifecycle state in canonical billing.",
};

pub const BILLING_REFUND_MANAGE_GRANT: BillingGrant = BillingGrant {
    key: "billing.refund.manage",
    resource: "billing.refund",
    action: "manage",
    description: "Issue and track refunds through billing providers.",
};

pub const BILLING_DOCUMENT_READ_GRANT: BillingGrant = BillingGrant {
    key: "billing.document.read",
    resource: "billing.document",
    action: "read",
    description: "Read invoices, receipts, and other billing documents.",
};

pub const BILLING_PROVIDER_ADMIN_GRANT: BillingGrant = BillingGrant {
    key: "billing.provider.admin",
    resource: "billing.provider",
    action: "admin",
    description: "Administer billing-provider connections, webhooks, and profiles.",
};

pub const PAYMENT_LINK_PAID_GRANT: BillingGrant = BillingGrant {
    key: "payment-link.paid",
    resource: "payment-link",
    action: "paid",
    description: "Grant access when a payment-link payment has succeeded.",
};

pub const PAYMENT_LINK_FAILED_GRANT: BillingGrant = BillingGrant {
    key: "payment-link.failed",
    resource: "payment-link",
    action: "failed",
    description: "Grant failure-handling access when a payment-link payment fails.",
};

pub const PAYMENT_PAID_GRANT: BillingGrant = BillingGrant {
    key: "payment.paid",
    resource: "payment",
    action: "paid",
    description: "Grant access when a canonical payment reaches a paid state.",
};

pub const PAYMENT_FAILED_GRANT: BillingGrant = BillingGrant {
    key: "payment.failed",
    resource: "payment",
    action: "failed",
    description: "Grant access when a canonical payment fails.",
};

pub const PAYMENT_REFUNDED_GRANT: BillingGrant = BillingGrant {
    key: "payment.refunded",
    resource: "payment",
    action: "refunded",
    description: "Grant access when a canonical payment is refunded.",
};

pub const SUBSCRIPTION_ACTIVE_GRANT: BillingGrant = BillingGrant {
    key: "subscription.active",
    resource: "subscription",
    action: "active",
    description: "Grant access when a subscription is active.",
};

pub const SUBSCRIPTION_CANCELED_GRANT: BillingGrant = BillingGrant {
    key: "subscription.canceled",
    resource: "subscription",
    action: "canceled",
    description: "Grant access when a subscription is canceled.",
};

pub const SUBSCRIPTION_PAST_DUE_GRANT: BillingGrant = BillingGrant {
    key: "subscription.past-due",
    resource: "subscription",
    action: "past-due",
    description: "Grant access when a subscription becomes past due.",
};

pub const INVOICE_OPEN_GRANT: BillingGrant = BillingGrant {
    key: "invoice.open",
    resource: "invoice",
    action: "open",
    description: "Grant access when an invoice is opened or issued.",
};

pub const INVOICE_PAID_GRANT: BillingGrant = BillingGrant {
    key: "invoice.paid",
    resource: "invoice",
    action: "paid",
    description: "Grant access when an invoice has been paid.",
};

pub const BILLING_GRANT_CATALOG: &[BillingGrant] = &[
    BILLING_READ_GRANT,
    BILLING_MANAGE_GRANT,
    BILLING_CUSTOMER_MANAGE_GRANT,
    BILLING_SUBSCRIPTION_MANAGE_GRANT,
    BILLING_REFUND_MANAGE_GRANT,
    BILLING_DOCUMENT_READ_GRANT,
    BILLING_PROVIDER_ADMIN_GRANT,
    PAYMENT_LINK_PAID_GRANT,
    PAYMENT_LINK_FAILED_GRANT,
    PAYMENT_PAID_GRANT,
    PAYMENT_FAILED_GRANT,
    PAYMENT_REFUNDED_GRANT,
    SUBSCRIPTION_ACTIVE_GRANT,
    SUBSCRIPTION_CANCELED_GRANT,
    SUBSCRIPTION_PAST_DUE_GRANT,
    INVOICE_OPEN_GRANT,
    INVOICE_PAID_GRANT,
];

pub fn billing_grant_catalog() -> &'static [BillingGrant] {
    BILLING_GRANT_CATALOG
}

pub fn billing_right_catalog() -> &'static [BillingRight] {
    billing_grant_catalog()
}

pub fn billing_grant_by_key(key: &str) -> Option<&'static BillingGrant> {
    BILLING_GRANT_CATALOG.iter().find(|grant| grant.key == key)
}

pub fn billing_right_by_key(key: &str) -> Option<&'static BillingRight> {
    billing_grant_by_key(key)
}

pub fn billing_right_modules_for_keys<'a>(
    grant_keys: impl IntoIterator<Item = &'a str>,
    right_keys: impl IntoIterator<Item = &'a str>,
) -> Vec<BillingRightModuleInput> {
    let right_keys = grant_keys
        .into_iter()
        .chain(right_keys)
        .map(str::trim)
        .filter(|key| !key.is_empty())
        .filter(|key| billing_right_by_key(key).is_some())
        .map(ToString::to_string)
        .collect::<BTreeSet<_>>()
        .into_iter()
        .collect::<Vec<_>>();

    if right_keys.is_empty() {
        Vec::new()
    } else {
        vec![BillingRightModuleInput {
            key: "billing".to_string(),
            right_keys,
        }]
    }
}

pub fn billing_permission_object_for_grants<'a>(
    grants: impl IntoIterator<Item = &'a str>,
) -> Value {
    let mut permissions = Map::new();

    for grant in grants {
        let Some(descriptor) = billing_grant_by_key(grant) else {
            continue;
        };

        match permissions.get_mut(descriptor.resource) {
            Some(Value::Array(actions)) => {
                if !actions
                    .iter()
                    .any(|value| value.as_str() == Some(descriptor.action))
                {
                    actions.push(Value::String(descriptor.action.to_string()));
                }
            }
            _ => {
                permissions.insert(descriptor.resource.to_string(), json!([descriptor.action]));
            }
        }
    }

    Value::Object(permissions)
}

pub fn billing_permission_object_for_rights<'a>(
    rights: impl IntoIterator<Item = &'a str>,
) -> Value {
    billing_permission_object_for_grants(rights)
}

pub fn billing_grant_projection_for_document(
    document: &CanonicalBillingDocument,
) -> BillingGrantProjection {
    match document {
        CanonicalBillingDocument::Payment(payment) => billing_grant_projection_for_payment(payment),
        CanonicalBillingDocument::Subscription(subscription) => {
            billing_grant_projection_for_subscription(subscription)
        }
        CanonicalBillingDocument::Invoice(invoice) => billing_grant_projection_for_invoice(invoice),
    }
}

pub fn billing_right_projection_for_document(
    document: &CanonicalBillingDocument,
) -> BillingRightProjection {
    billing_grant_projection_for_document(document)
}

pub fn billing_auth_grant_sync_request(
    subject: &BillingSubjectRef,
    projection: &BillingGrantProjection,
    metadata: Option<&Value>,
) -> BillingAuthGrantSyncRequest {
    BillingAuthGrantSyncRequest {
        subject_kind: match subject.owner_kind {
            crate::BillingOwnerKind::User => "user",
            crate::BillingOwnerKind::Org => "org",
            crate::BillingOwnerKind::Workspace => "workspace",
            crate::BillingOwnerKind::Tenant => "tenant",
        }
        .to_string(),
        subject_id: subject.owner_id.clone(),
        source_kind: projection.source_kind.as_str().to_string(),
        source_ref: projection.source_ref.clone(),
        right_modules: projection.right_modules.clone(),
        right_keys: projection.right_keys.clone(),
        grant_keys: projection.grant_keys.clone(),
        metadata: merge_projection_metadata(&projection.metadata, metadata),
    }
}

pub fn billing_auth_right_sync_request(
    subject: &BillingSubjectRef,
    projection: &BillingRightProjection,
    metadata: Option<&Value>,
) -> BillingAuthRightSyncRequest {
    billing_auth_grant_sync_request(subject, projection, metadata)
}

fn merge_projection_metadata(base: &Value, extra: Option<&Value>) -> Value {
    match (base, extra) {
        (Value::Object(base_map), Some(Value::Object(extra_map))) => {
            let mut merged = base_map.clone();
            merged.extend(extra_map.clone());
            Value::Object(merged)
        }
        (Value::Object(base_map), _) => Value::Object(base_map.clone()),
        (_, Some(extra)) if !extra.is_null() => extra.clone(),
        (base, _) => base.clone(),
    }
}

pub fn billing_grant_projection_for_payment(payment: &CanonicalPayment) -> BillingGrantProjection {
    let grant_keys = crate::grant_keys_for_payment_status(
        payment.status,
        payment.provider_payment_link_id.is_some(),
    )
    .into_iter()
    .map(ToString::to_string)
    .collect::<Vec<_>>();

    BillingGrantProjection {
        source_kind: BillingGrantSourceKind::Payment,
        source_ref: payment.provider_payment_id.clone(),
        right_modules: billing_right_modules_for_keys(
            grant_keys.iter().map(String::as_str),
            std::iter::empty(),
        ),
        right_keys: grant_keys.clone(),
        permissions: billing_permission_object_for_grants(grant_keys.iter().map(String::as_str)),
        metadata: json!({
            "documentKind": "payment",
            "provider": payment.provider.as_str(),
            "status": payment.status.as_str(),
            "providerPaymentId": payment.provider_payment_id,
            "providerCustomerId": payment.provider_customer_id,
            "providerProfileId": payment.provider_profile_id,
            "providerSubscriptionId": payment.provider_subscription_id,
            "providerPaymentLinkId": payment.provider_payment_link_id,
            "isPaymentLink": payment.provider_payment_link_id.is_some(),
        }),
        grant_keys,
    }
}

pub fn billing_right_projection_for_payment(payment: &CanonicalPayment) -> BillingRightProjection {
    billing_grant_projection_for_payment(payment)
}

pub fn billing_grant_projection_for_subscription(
    subscription: &CanonicalSubscription,
) -> BillingGrantProjection {
    let grant_keys = crate::grant_keys_for_subscription_status(subscription.status)
        .into_iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>();

    BillingGrantProjection {
        source_kind: BillingGrantSourceKind::Subscription,
        source_ref: subscription.provider_subscription_id.clone(),
        right_modules: billing_right_modules_for_keys(
            grant_keys.iter().map(String::as_str),
            std::iter::empty(),
        ),
        right_keys: grant_keys.clone(),
        permissions: billing_permission_object_for_grants(grant_keys.iter().map(String::as_str)),
        metadata: json!({
            "documentKind": "subscription",
            "provider": subscription.provider.as_str(),
            "status": subscription.status.as_str(),
            "providerSubscriptionId": subscription.provider_subscription_id,
            "providerCustomerId": subscription.provider_customer_id,
            "providerProfileId": subscription.provider_profile_id,
        }),
        grant_keys,
    }
}

pub fn billing_right_projection_for_subscription(
    subscription: &CanonicalSubscription,
) -> BillingRightProjection {
    billing_grant_projection_for_subscription(subscription)
}

pub fn billing_grant_projection_for_invoice(invoice: &CanonicalInvoice) -> BillingGrantProjection {
    let grant_keys = crate::grant_keys_for_invoice_status(invoice.status)
        .into_iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>();

    BillingGrantProjection {
        source_kind: BillingGrantSourceKind::Invoice,
        source_ref: invoice.provider_invoice_id.clone(),
        right_modules: billing_right_modules_for_keys(
            grant_keys.iter().map(String::as_str),
            std::iter::empty(),
        ),
        right_keys: grant_keys.clone(),
        permissions: billing_permission_object_for_grants(grant_keys.iter().map(String::as_str)),
        metadata: json!({
            "documentKind": "invoice",
            "provider": invoice.provider.as_str(),
            "status": invoice.status.as_str(),
            "providerInvoiceId": invoice.provider_invoice_id,
            "providerCustomerId": invoice.provider_customer_id,
            "providerProfileId": invoice.provider_profile_id,
        }),
        grant_keys,
    }
}

pub fn billing_right_projection_for_invoice(invoice: &CanonicalInvoice) -> BillingRightProjection {
    billing_grant_projection_for_invoice(invoice)
}

#[cfg(test)]
mod tests {
    use super::{
        BillingGrantSourceKind, billing_auth_grant_sync_request, billing_auth_right_sync_request,
        billing_grant_catalog, billing_grant_projection_for_document,
        billing_permission_object_for_grants, billing_permission_object_for_rights,
        billing_right_catalog, billing_right_modules_for_keys,
        billing_right_projection_for_document,
    };
    use crate::canonical::{
        CanonicalBillingDocument, CanonicalInvoice, CanonicalMoney, CanonicalPayment,
        CanonicalSubscription,
    };
    use crate::{
        AthenaInvoiceStatus, AthenaPaymentStatus, AthenaSubscriptionStatus, BillingOwnerKind,
        BillingProvider, BillingSubjectRef,
    };
    use serde_json::json;

    #[test]
    fn payment_document_projection_expands_source_and_payment_link_grants() {
        let document = CanonicalBillingDocument::Payment(CanonicalPayment {
            provider: BillingProvider::Mollie,
            provider_payment_id: "pay_123".to_string(),
            provider_customer_id: Some("cst_123".to_string()),
            provider_profile_id: Some("pfl_123".to_string()),
            provider_subscription_id: Some("sub_123".to_string()),
            provider_payment_link_id: Some("pl_123".to_string()),
            status: AthenaPaymentStatus::Paid,
            amount: CanonicalMoney {
                currency: "EUR".to_string(),
                value: "24.95".to_string(),
            },
            amount_refunded: None,
            description: Some("Bike tires".to_string()),
            metadata: json!({"campaign":"spring"}),
            paid_at: None,
            created_at: None,
            raw: json!({}),
        });

        let projection = billing_grant_projection_for_document(&document);
        assert_eq!(projection.source_kind, BillingGrantSourceKind::Payment);
        assert_eq!(projection.source_ref, "pay_123");
        assert_eq!(
            projection.grant_keys,
            vec!["payment.paid", "payment-link.paid"]
        );
        assert_eq!(
            projection.right_keys,
            vec!["payment.paid", "payment-link.paid"]
        );
        assert_eq!(projection.right_modules.len(), 1);
        assert_eq!(projection.right_modules[0].key, "billing");
        assert_eq!(
            projection.right_modules[0].right_keys,
            vec!["payment-link.paid".to_string(), "payment.paid".to_string()]
        );
        assert_eq!(
            projection.permissions,
            json!({
                "payment": ["paid"],
                "payment-link": ["paid"]
            })
        );
        assert_eq!(projection.metadata["isPaymentLink"], true);
    }

    #[test]
    fn subscription_and_invoice_projection_statuses_map_to_permissions() {
        let subscription = CanonicalBillingDocument::Subscription(CanonicalSubscription {
            provider: BillingProvider::Mollie,
            provider_subscription_id: "sub_123".to_string(),
            provider_customer_id: "cst_123".to_string(),
            provider_profile_id: None,
            status: AthenaSubscriptionStatus::PastDue,
            amount: None,
            currency: None,
            interval: None,
            description: None,
            metadata: json!({}),
            next_payment_date: None,
            created_at: None,
            canceled_at: None,
            raw: json!({}),
        });
        let invoice = CanonicalBillingDocument::Invoice(CanonicalInvoice {
            provider: BillingProvider::Stripe,
            provider_invoice_id: "inv_123".to_string(),
            provider_profile_id: None,
            provider_customer_id: Some("cus_123".to_string()),
            status: AthenaInvoiceStatus::Paid,
            amount: None,
            amount_paid: None,
            description: None,
            metadata: json!({}),
            issued_at: None,
            paid_at: None,
            created_at: None,
            raw: json!({}),
        });

        let subscription_projection = billing_grant_projection_for_document(&subscription);
        let invoice_projection = billing_grant_projection_for_document(&invoice);

        assert_eq!(
            subscription_projection.grant_keys,
            vec!["subscription.past-due"]
        );
        assert_eq!(
            subscription_projection.right_keys,
            vec!["subscription.past-due"]
        );
        assert_eq!(invoice_projection.grant_keys, vec!["invoice.paid"]);
        assert_eq!(invoice_projection.right_keys, vec!["invoice.paid"]);
        assert_eq!(subscription_projection.metadata["status"], "past_due");
        assert_eq!(invoice_projection.metadata["status"], "paid");
    }

    #[test]
    fn permissions_object_ignores_unknown_grants() {
        assert_eq!(
            billing_permission_object_for_grants(["payment.paid", "missing.grant"]),
            json!({"payment": ["paid"]})
        );
        assert_eq!(
            billing_permission_object_for_rights(["payment.paid", "missing.grant"]),
            json!({"payment": ["paid"]})
        );
    }

    #[test]
    fn auth_grant_sync_request_uses_subject_and_projection_contract() {
        let projection = billing_grant_projection_for_document(&CanonicalBillingDocument::Payment(
            CanonicalPayment {
                provider: BillingProvider::Mollie,
                provider_payment_id: "pay_123".to_string(),
                provider_customer_id: Some("cst_123".to_string()),
                provider_profile_id: Some("pfl_123".to_string()),
                provider_subscription_id: Some("sub_123".to_string()),
                provider_payment_link_id: Some("pl_123".to_string()),
                status: AthenaPaymentStatus::Paid,
                amount: CanonicalMoney {
                    currency: "EUR".to_string(),
                    value: "24.95".to_string(),
                },
                amount_refunded: None,
                description: Some("Bike tires".to_string()),
                metadata: json!({}),
                paid_at: None,
                created_at: None,
                raw: json!({}),
            },
        ));
        let sync = billing_auth_grant_sync_request(
            &BillingSubjectRef {
                owner_kind: BillingOwnerKind::Workspace,
                owner_id: "ws_123".to_string(),
            },
            &projection,
            Some(&json!({ "origin": "webhook" })),
        );

        assert_eq!(sync.subject_kind, "workspace");
        assert_eq!(sync.subject_id, "ws_123");
        assert_eq!(sync.source_kind, "payment");
        assert_eq!(sync.source_ref, "pay_123");
        assert_eq!(sync.right_modules.len(), 1);
        assert_eq!(sync.right_modules[0].key, "billing");
        assert_eq!(sync.right_keys, vec!["payment.paid", "payment-link.paid"]);
        assert_eq!(sync.grant_keys, vec!["payment.paid", "payment-link.paid"]);
        assert_eq!(sync.metadata["origin"], "webhook");
        assert_eq!(sync.metadata["providerSubscriptionId"], "sub_123");
        assert_eq!(sync.metadata["providerPaymentLinkId"], "pl_123");
    }

    #[test]
    fn right_aliases_match_grant_first_projection_contracts() {
        let document = CanonicalBillingDocument::Payment(CanonicalPayment {
            provider: BillingProvider::Mollie,
            provider_payment_id: "pay_123".to_string(),
            provider_customer_id: Some("cst_123".to_string()),
            provider_profile_id: Some("pfl_123".to_string()),
            provider_subscription_id: Some("sub_123".to_string()),
            provider_payment_link_id: Some("pl_123".to_string()),
            status: AthenaPaymentStatus::Paid,
            amount: CanonicalMoney {
                currency: "EUR".to_string(),
                value: "24.95".to_string(),
            },
            amount_refunded: None,
            description: Some("Bike tires".to_string()),
            metadata: json!({}),
            paid_at: None,
            created_at: None,
            raw: json!({}),
        });
        let subject = BillingSubjectRef {
            owner_kind: BillingOwnerKind::User,
            owner_id: "user_123".to_string(),
        };

        let grant_projection = billing_grant_projection_for_document(&document);
        let right_projection = billing_right_projection_for_document(&document);
        let grant_sync = billing_auth_grant_sync_request(&subject, &grant_projection, None);
        let right_sync = billing_auth_right_sync_request(&subject, &right_projection, None);

        assert_eq!(billing_right_catalog(), billing_grant_catalog());
        assert_eq!(right_projection, grant_projection);
        assert_eq!(right_sync, grant_sync);
    }

    #[test]
    fn billing_right_module_inputs_group_resolved_billing_keys() {
        let modules = billing_right_modules_for_keys(
            ["payment-link.paid", "payment.paid", "unknown.right"],
            std::iter::empty(),
        );

        assert_eq!(modules.len(), 1);
        assert_eq!(modules[0].key, "billing");
        assert_eq!(
            modules[0].right_keys,
            vec!["payment-link.paid".to_string(), "payment.paid".to_string()]
        );
    }
}
