use crate::contract::Core4Mica;
use alloy::contract as alloy_contract;
use alloy::primitives::{Address, Bytes};
use anyhow::Error;
use crypto::hex::DecodeHexError;
use reqwest::StatusCode;
use rpc::ApiClientError;
use serde_json::Value;
use thiserror::Error;
use url::ParseError;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("invalid config value: {0}")]
    InvalidValue(String),
    #[error("missing config: {0}")]
    Missing(String),
}

#[derive(Error, Debug)]
pub enum AuthError {
    #[error("invalid auth URL: {0}")]
    InvalidUrl(#[from] ParseError),
    #[error("auth request failed: {0}")]
    Transport(#[from] reqwest::Error),
    #[error("failed to decode auth response: {0}")]
    Decode(#[from] serde_json::Error),
    #[error("auth server returned {status}: {message}")]
    Api { status: StatusCode, message: String },
    #[error("signing failed: {0}")]
    Signing(String),
    #[error("auth config is missing")]
    MissingConfig,
    #[error("refresh token not available")]
    MissingRefreshToken,
    #[error("auth state error: {0}")]
    Internal(String),
}

impl From<AuthError> for ApiClientError {
    fn from(val: AuthError) -> Self {
        let status = match &val {
            AuthError::Api { status, .. } => *status,
            AuthError::InvalidUrl(_) | AuthError::MissingConfig => StatusCode::BAD_REQUEST,
            AuthError::MissingRefreshToken => StatusCode::UNAUTHORIZED,
            AuthError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE,
            AuthError::Decode(_) => StatusCode::BAD_GATEWAY,
            AuthError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
            AuthError::Signing(_) => StatusCode::UNAUTHORIZED,
        };

        match val {
            AuthError::InvalidUrl(err) => ApiClientError::InvalidUrl(err),
            AuthError::Transport(err) => ApiClientError::Transport(err),
            AuthError::Decode(err) => ApiClientError::Decode(err),
            AuthError::Api { status, message } => ApiClientError::Api { status, message },
            other => ApiClientError::Api {
                status,
                message: other.to_string(),
            },
        }
    }
}

#[derive(Error, Debug)]
pub enum ClientError {
    #[error("client RPC error: {0}")]
    Rpc(String),

    #[error("client provider error: {0}")]
    Provider(String),

    #[error("client initialization error: {0}")]
    Initialization(String),
}

#[derive(Debug, Error)]
pub enum SignPaymentError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("address mismatch: signer={signer:?} != claims.user_address={claims}")]
    AddressMismatch { signer: Address, claims: String },
    #[error("invalid user address in claims")]
    InvalidUserAddress,
    #[error("invalid recipient address in claims")]
    InvalidRecipientAddress,
    #[error("failed to sign the payment: {0}")]
    Failed(String),

    #[error(transparent)]
    Rpc(#[from] ApiClientError),
}

#[derive(Debug, Error)]
pub enum RemunerateError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("failed to decode guarantee claims hex")]
    ClaimsHex(#[source] Error),
    #[error("failed to decode guarantee claims")]
    ClaimsDecode(#[source] Error),
    #[error("failed to convert guarantee claims into contract type")]
    GuaranteeConversion(#[source] Error),
    #[error("failed to decode signature hex")]
    SignatureHex(#[source] DecodeHexError),
    #[error("failed to decode BLS signature")]
    SignatureDecode(#[source] Error),
    #[error("tab not yet overdue")]
    TabNotYetOverdue,
    #[error("tab expired")]
    TabExpired,
    #[error("tab previously remunerated")]
    TabPreviouslyRemunerated,
    #[error("tab already paid")]
    TabAlreadyPaid,
    #[error("invalid signature")]
    InvalidSignature,
    #[error("double spending detected")]
    DoubleSpendingDetected,
    #[error("invalid recipient")]
    InvalidRecipient,
    #[error("amount is zero")]
    AmountZero,
    #[error("transfer failed")]
    TransferFailed,
    #[error("certificate verification failed: {0}")]
    CertificateInvalid(#[source] Error),
    #[error("certificate signature mismatch before submission")]
    CertificateMismatch,
    #[error("guarantee version mismatch: expected {expected}, got {actual}")]
    GuaranteeVersionMismatch { expected: u64, actual: u64 },
    #[error("guarantee domain mismatch")]
    GuaranteeDomainMismatch,
    #[error("unsupported guarantee version: {0}")]
    UnsupportedGuaranteeVersion(u64),
    #[error("invalid min validation score")]
    InvalidMinValidationScore,
    #[error("invalid validation chain id")]
    InvalidValidationChainId,
    #[error("untrusted validation registry: {0}")]
    UntrustedValidationRegistry(Address),
    #[error("validation subject hash mismatch")]
    ValidationSubjectHashMismatch,
    #[error("validation request hash mismatch")]
    ValidationRequestHashMismatch,
    #[error("validation lookup failed")]
    ValidationLookupFailed,
    #[error("validation pending")]
    ValidationPending,
    #[error("validation score too low")]
    ValidationScoreTooLow,
    #[error("validation validator mismatch")]
    ValidationValidatorMismatch,
    #[error("validation agent mismatch")]
    ValidationAgentMismatch,
    #[error("validation tag mismatch")]
    ValidationTagMismatch,

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum FinalizeWithdrawalError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("no withdrawal requested")]
    NoWithdrawalRequested,
    #[error("grace period not elapsed")]
    GracePeriodNotElapsed,
    #[error("transfer failed")]
    TransferFailed,
    #[error("unsupported asset: {0}")]
    UnsupportedAsset(Address),
    #[error("stablecoin withdraw shortfall for {asset}: requested {requested}, actual {actual}")]
    StablecoinWithdrawShortfall {
        asset: Address,
        requested: String,
        actual: String,
    },

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum RequestWithdrawalError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("amount is zero")]
    AmountZero,
    #[error("insufficient available")]
    InsufficientAvailable,
    #[error("unsupported asset: {0}")]
    UnsupportedAsset(Address),

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum CancelWithdrawalError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("no withdrawal requested")]
    NoWithdrawalRequested,

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum DepositError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("amount is zero")]
    AmountZero,
    #[error("unsupported asset: {0}")]
    UnsupportedAsset(Address),
    #[error("illegal value")]
    IllegalValue,
    #[error("Aave is not configured")]
    AaveNotConfigured,

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum ApproveErc20Error {
    #[error("invalid params: {0}")]
    InvalidParams(String),

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum PayTabError {
    #[error("invalid params: {0}")]
    InvalidParams(String),
    #[error("invalid asset")]
    InvalidAsset,
    #[error("unsupported asset: {0}")]
    UnsupportedAsset(Address),

    #[error(transparent)]
    Client(#[from] ClientError),

    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum GetUserError {
    #[error("unsupported asset: {0}")]
    UnsupportedAsset(Address),
    #[error("Aave is not configured")]
    AaveNotConfigured,
    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum TabPaymentStatusError {
    #[error("unknown revert (selector {selector:#x})")]
    UnknownRevert { selector: u32, data: Vec<u8> },
    #[error("provider/transport error: {0}")]
    Transport(String),
}

#[derive(Debug, Error)]
pub enum CreateTabError {
    #[error("invalid params: {0}")]
    InvalidParams(String),

    #[error(transparent)]
    Rpc(#[from] ApiClientError),
}

#[derive(Debug, Error)]
pub enum IssuePaymentGuaranteeError {
    #[error("invalid params: {0}")]
    InvalidParams(String),

    #[error(transparent)]
    Rpc(#[from] ApiClientError),
}

#[derive(Debug, Error)]
pub enum RecipientQueryError {
    #[error(transparent)]
    Rpc(#[from] ApiClientError),
}

#[derive(Debug, Error)]
pub enum VerifyGuaranteeError {
    #[error("invalid BLS certificate")]
    InvalidCertificate(#[source] Error),
    #[error("certificate signature mismatch")]
    CertificateMismatch,
    #[error("guarantee version mismatch: expected {expected}, got {actual}")]
    GuaranteeVersionMismatch { expected: u64, actual: u64 },
    #[error("guarantee domain mismatch")]
    GuaranteeDomainMismatch,
    #[error("unsupported guarantee version: {0}")]
    UnsupportedGuaranteeVersion(u64),
}

#[derive(Debug, Error)]
pub enum X402Error {
    #[error("invalid scheme: {0}")]
    InvalidScheme(String),
    #[error("invalid x402 version: {0}")]
    InvalidVersion(String),
    #[error("invalid facilitator url: {0}")]
    InvalidFacilitatorUrl(String),
    #[error("failed to resolve tab endpoint: {0}")]
    TabResolutionFailed(String),
    #[error("failed to encode payment envelope: {0}")]
    EncodeEnvelope(String),
    #[error("invalid paymentRequirements.extra: {0}")]
    InvalidExtra(String),
    #[error("invalid number for field {field}: {source}")]
    InvalidNumber { field: String, source: Error },
    #[error("user mismatch in paymentRequirements: found {found}, expected {expected}")]
    UserMismatch { found: String, expected: String },
    #[error("settlement failed with status {status}: {body}")]
    SettlementFailed { status: StatusCode, body: Value },
    #[error(transparent)]
    Signing(#[from] SignPaymentError),
    #[error(transparent)]
    Http(#[from] reqwest::Error),
}

/// Minimal context for a revert we could decode from the contract call.
#[derive(Debug, Clone)]
struct RevertDetails {
    selector: u32,
    data: Vec<u8>,
}

impl RevertDetails {
    fn from_error(e: &alloy_contract::Error) -> Option<Self> {
        e.as_revert_data().map(|bytes: Bytes| {
            let data = bytes.to_vec();
            let selector = if data.len() >= 4 {
                u32::from_be_bytes([data[0], data[1], data[2], data[3]])
            } else {
                0
            };
            Self { selector, data }
        })
    }
}

const INVALID_MIN_VALIDATION_SCORE_SELECTOR: u32 = 0x940e8e0e;
const INVALID_VALIDATION_CHAIN_ID_SELECTOR: u32 = 0xabe8d799;
const UNTRUSTED_VALIDATION_REGISTRY_SELECTOR: u32 = 0x6098bbe0;
const VALIDATION_SUBJECT_HASH_MISMATCH_SELECTOR: u32 = 0xd7201f6e;
const VALIDATION_REQUEST_HASH_MISMATCH_SELECTOR: u32 = 0x95ce60ab;
const VALIDATION_LOOKUP_FAILED_SELECTOR: u32 = 0x105163d4;
const VALIDATION_PENDING_SELECTOR: u32 = 0x860263f8;
const VALIDATION_SCORE_TOO_LOW_SELECTOR: u32 = 0xf44670f9;
const VALIDATION_VALIDATOR_MISMATCH_SELECTOR: u32 = 0x9e8eb320;
const VALIDATION_AGENT_MISMATCH_SELECTOR: u32 = 0xe474a924;
const VALIDATION_TAG_MISMATCH_SELECTOR: u32 = 0x0604e144;

trait ContractErrorTarget {
    fn from_unknown_revert(revert: RevertDetails) -> Self;
    fn from_transport(err: alloy_contract::Error) -> Self;
}

fn map_contract_error<T, F>(error: alloy_contract::Error, map_decoded: F) -> T
where
    T: ContractErrorTarget,
    F: FnOnce(Core4Mica::Core4MicaErrors) -> Option<T>,
{
    if let Some(decoded) = error.as_decoded_interface_error::<Core4Mica::Core4MicaErrors>()
        && let Some(mapped) = map_decoded(decoded)
    {
        return mapped;
    }

    if let Some(revert) = RevertDetails::from_error(&error) {
        return T::from_unknown_revert(revert);
    }

    T::from_transport(error)
}

macro_rules! impl_from_alloy_error {
    ($target:ty, { $($contract_err:pat => $target_err:expr),* $(,)? }) => {
        impl From<alloy_contract::Error> for $target {
            fn from(e: alloy_contract::Error) -> Self {
                map_contract_error(e, |decoded| match decoded {
                    $(
                        $contract_err => Some($target_err),
                    )*
                    _ => None,
                })
            }
        }
    };
    ($target:ty) => {
        impl From<alloy_contract::Error> for $target {
            fn from(e: alloy_contract::Error) -> Self {
                map_contract_error(e, |_| None)
            }
        }
    };
}

macro_rules! impl_contract_error_target {
    ($target:ty) => {
        impl ContractErrorTarget for $target {
            fn from_unknown_revert(revert: RevertDetails) -> Self {
                Self::UnknownRevert {
                    selector: revert.selector,
                    data: revert.data,
                }
            }

            fn from_transport(err: alloy_contract::Error) -> Self {
                Self::Transport(err.to_string())
            }
        }
    };
}

impl_contract_error_target!(RemunerateError);
impl_contract_error_target!(FinalizeWithdrawalError);
impl_contract_error_target!(RequestWithdrawalError);
impl_contract_error_target!(CancelWithdrawalError);
impl_contract_error_target!(DepositError);
impl_contract_error_target!(ApproveErc20Error);
impl_contract_error_target!(PayTabError);
impl_contract_error_target!(GetUserError);
impl_contract_error_target!(TabPaymentStatusError);

impl From<alloy_contract::Error> for RemunerateError {
    fn from(error: alloy_contract::Error) -> Self {
        if let Some(decoded) = error.as_decoded_interface_error::<Core4Mica::Core4MicaErrors>() {
            match decoded {
                Core4Mica::Core4MicaErrors::TabNotYetOverdue(_) => return Self::TabNotYetOverdue,
                Core4Mica::Core4MicaErrors::TabExpired(_) => return Self::TabExpired,
                Core4Mica::Core4MicaErrors::TabPreviouslyRemunerated(_) => {
                    return Self::TabPreviouslyRemunerated;
                }
                Core4Mica::Core4MicaErrors::TabAlreadyPaid(_) => return Self::TabAlreadyPaid,
                Core4Mica::Core4MicaErrors::InvalidSignature(_) => return Self::InvalidSignature,
                Core4Mica::Core4MicaErrors::DoubleSpendingDetected(_) => {
                    return Self::DoubleSpendingDetected;
                }
                Core4Mica::Core4MicaErrors::InvalidRecipient(_) => return Self::InvalidRecipient,
                Core4Mica::Core4MicaErrors::AmountZero(_) => return Self::AmountZero,
                Core4Mica::Core4MicaErrors::TransferFailed(_) => return Self::TransferFailed,
                Core4Mica::Core4MicaErrors::InvalidGuaranteeDomain(_) => {
                    return Self::GuaranteeDomainMismatch;
                }
                Core4Mica::Core4MicaErrors::UnsupportedGuaranteeVersion(err) => {
                    return Self::UnsupportedGuaranteeVersion(err.version);
                }
                _ => {}
            }
        }

        if let Some(revert) = RevertDetails::from_error(&error) {
            return match revert.selector {
                INVALID_MIN_VALIDATION_SCORE_SELECTOR => Self::InvalidMinValidationScore,
                INVALID_VALIDATION_CHAIN_ID_SELECTOR => Self::InvalidValidationChainId,
                UNTRUSTED_VALIDATION_REGISTRY_SELECTOR => decode_address_argument(&revert.data)
                    .map(Self::UntrustedValidationRegistry)
                    .unwrap_or(Self::UnknownRevert {
                        selector: revert.selector,
                        data: revert.data,
                    }),
                VALIDATION_SUBJECT_HASH_MISMATCH_SELECTOR => Self::ValidationSubjectHashMismatch,
                VALIDATION_REQUEST_HASH_MISMATCH_SELECTOR => Self::ValidationRequestHashMismatch,
                VALIDATION_LOOKUP_FAILED_SELECTOR => Self::ValidationLookupFailed,
                VALIDATION_PENDING_SELECTOR => Self::ValidationPending,
                VALIDATION_SCORE_TOO_LOW_SELECTOR => Self::ValidationScoreTooLow,
                VALIDATION_VALIDATOR_MISMATCH_SELECTOR => Self::ValidationValidatorMismatch,
                VALIDATION_AGENT_MISMATCH_SELECTOR => Self::ValidationAgentMismatch,
                VALIDATION_TAG_MISMATCH_SELECTOR => Self::ValidationTagMismatch,
                _ => Self::UnknownRevert {
                    selector: revert.selector,
                    data: revert.data,
                },
            };
        }

        Self::Transport(error.to_string())
    }
}

fn decode_address_argument(data: &[u8]) -> Option<Address> {
    if data.len() < 36 {
        return None;
    }

    let mut address = [0u8; 20];
    address.copy_from_slice(&data[16..36]);
    Some(Address::from(address))
}

impl_from_alloy_error!(FinalizeWithdrawalError, {
    Core4Mica::Core4MicaErrors::NoWithdrawalRequested(_) => Self::NoWithdrawalRequested,
    Core4Mica::Core4MicaErrors::GracePeriodNotElapsed(_) => Self::GracePeriodNotElapsed,
    Core4Mica::Core4MicaErrors::TransferFailed(_) => Self::TransferFailed,
    Core4Mica::Core4MicaErrors::UnsupportedAsset(err) => Self::UnsupportedAsset(err.asset),
    Core4Mica::Core4MicaErrors::StablecoinWithdrawShortfall(err) => Self::StablecoinWithdrawShortfall {
        asset: err.asset,
        requested: err.requested.to_string(),
        actual: err.actual.to_string(),
    },
});

impl_from_alloy_error!(RequestWithdrawalError, {
    Core4Mica::Core4MicaErrors::AmountZero(_) => Self::AmountZero,
    Core4Mica::Core4MicaErrors::InsufficientAvailable(_) => Self::InsufficientAvailable,
    Core4Mica::Core4MicaErrors::UnsupportedAsset(err) => Self::UnsupportedAsset(err.asset),
});

impl_from_alloy_error!(CancelWithdrawalError, {
    Core4Mica::Core4MicaErrors::NoWithdrawalRequested(_) => Self::NoWithdrawalRequested,
});

impl_from_alloy_error!(DepositError, {
    Core4Mica::Core4MicaErrors::AmountZero(_) => Self::AmountZero,
    Core4Mica::Core4MicaErrors::UnsupportedAsset(err) => Self::UnsupportedAsset(err.asset),
    Core4Mica::Core4MicaErrors::IllegalValue(_) => Self::IllegalValue,
    Core4Mica::Core4MicaErrors::AaveNotConfigured(_) => Self::AaveNotConfigured,
});

impl_from_alloy_error!(PayTabError, {
    Core4Mica::Core4MicaErrors::InvalidAsset(_) => Self::InvalidAsset,
    Core4Mica::Core4MicaErrors::UnsupportedAsset(err) => Self::UnsupportedAsset(err.asset),
});

impl_from_alloy_error!(ApproveErc20Error);

impl_from_alloy_error!(GetUserError, {
    Core4Mica::Core4MicaErrors::UnsupportedAsset(err) => Self::UnsupportedAsset(err.asset),
    Core4Mica::Core4MicaErrors::AaveNotConfigured(_) => Self::AaveNotConfigured,
});

impl_from_alloy_error!(TabPaymentStatusError);
