use async_graphql::{dataloader::*, types::connection::*, *};

use cala_ledger::{
    balance::*,
    entry::EntriesByCreatedAtCursor,
    primitives::{AccountId, Currency, JournalId},
};

pub use cala_ledger::account::AccountsByNameCursor;

use crate::app::CalaApp;

use super::{
    account_set::*, balance::Balance, convert::ToGlobalId, entry::Entry, loader::LedgerDataLoader,
    primitives::*, schema::DbOp,
};

#[derive(Clone, SimpleObject)]
#[graphql(complex)]
pub struct Account {
    id: ID,
    account_id: UUID,
    version: u32,
    code: String,
    name: String,
    normal_balance_type: DebitOrCredit,
    status: Status,
    external_id: Option<String>,
    description: Option<String>,
    metadata: Option<JSON>,
    pub(super) created_at: Timestamp,
    modified_at: Timestamp,
}

#[ComplexObject]
impl Account {
    async fn balance(
        &self,
        ctx: &Context<'_>,
        journal_id: UUID,
        currency: CurrencyCode,
    ) -> async_graphql::Result<Option<Balance>> {
        let journal_id = JournalId::from(journal_id);
        let account_id = AccountId::from(self.account_id);
        let currency = Currency::from(currency);

        let balance: Option<AccountBalance> = match ctx.data_opt::<DbOp>() {
            Some(op) => {
                let app = ctx.data_unchecked::<CalaApp>();
                let mut op = op.try_lock().expect("Lock held concurrently");
                Some(
                    app.ledger()
                        .balances()
                        .find_in_op(&mut op, journal_id, account_id, currency)
                        .await?,
                )
            }
            None => {
                let loader = ctx.data_unchecked::<DataLoader<LedgerDataLoader>>();
                loader.load_one((journal_id, account_id, currency)).await?
            }
        };
        Ok(balance.map(Balance::from))
    }

    async fn sets(
        &self,
        ctx: &Context<'_>,
        first: i32,
        after: Option<String>,
    ) -> Result<Connection<AccountSetsByNameCursor, AccountSet, EmptyFields, EmptyFields>> {
        let app = ctx.data_unchecked::<CalaApp>();
        let account_id = AccountId::from(self.account_id);
        query(
            after,
            None,
            Some(first),
            None,
            |after, _, first, _| async move {
                let first = first.expect("First always exists");
                let query_args = cala_ledger::es_entity::PaginatedQueryArgs { first, after };

                let result = match ctx.data_opt::<DbOp>() {
                    Some(op) => {
                        let mut op = op.try_lock().expect("Lock held concurrently");
                        app.ledger()
                            .account_sets()
                            .find_where_member_in_op(&mut op, account_id, query_args)
                            .await?
                    }
                    None => {
                        app.ledger()
                            .account_sets()
                            .find_where_member(account_id, query_args)
                            .await?
                    }
                };
                let mut connection = Connection::new(false, result.has_next_page);
                connection
                    .edges
                    .extend(result.entities.into_iter().map(|entity| {
                        let cursor = AccountSetsByNameCursor::from(&entity);
                        Edge::new(cursor, AccountSet::from(entity))
                    }));
                Ok::<_, async_graphql::Error>(connection)
            },
        )
        .await
    }

    async fn entries(
        &self,
        ctx: &Context<'_>,
        first: i32,
        after: Option<String>,
    ) -> Result<Connection<EntriesByCreatedAtCursor, Entry, EmptyFields, EmptyFields>> {
        let app = ctx.data_unchecked::<CalaApp>();
        let account_id = AccountId::from(self.account_id);
        query(
            after,
            None,
            Some(first),
            None,
            |after, _, first, _| async move {
                let first = first.expect("First always exists");
                let result = app
                    .ledger()
                    .entries()
                    .list_for_account_id(
                        account_id,
                        cala_ledger::es_entity::PaginatedQueryArgs { first, after },
                        cala_ledger::es_entity::ListDirection::Descending,
                    )
                    .await?;
                let mut connection = Connection::new(false, result.has_next_page);
                connection
                    .edges
                    .extend(result.entities.into_iter().map(|entity| {
                        let cursor = EntriesByCreatedAtCursor::from(&entity);
                        Edge::new(cursor, Entry::from(entity))
                    }));
                Ok::<_, async_graphql::Error>(connection)
            },
        )
        .await
    }
}

#[derive(InputObject)]
pub(super) struct AccountCreateInput {
    pub account_id: UUID,
    pub external_id: Option<String>,
    pub code: String,
    pub name: String,
    #[graphql(default)]
    pub normal_balance_type: DebitOrCredit,
    pub description: Option<String>,
    #[graphql(default)]
    pub status: Status,
    pub metadata: Option<JSON>,
    pub account_set_ids: Option<Vec<UUID>>,
}

#[derive(SimpleObject)]
pub(super) struct AccountCreatePayload {
    pub account: Account,
}

#[derive(InputObject)]
pub(super) struct AccountUpdateInput {
    pub external_id: Option<String>,
    pub code: Option<String>,
    pub name: Option<String>,
    pub normal_balance_type: Option<DebitOrCredit>,
    pub description: Option<String>,
    pub status: Option<Status>,
    pub metadata: Option<JSON>,
}

#[derive(SimpleObject)]
pub(super) struct AccountUpdatePayload {
    pub account: Account,
}

impl ToGlobalId for cala_ledger::AccountId {
    fn to_global_id(&self) -> async_graphql::types::ID {
        async_graphql::types::ID::from(format!("account:{}", self))
    }
}

impl From<cala_ledger::account::Account> for Account {
    fn from(account: cala_ledger::account::Account) -> Self {
        let created_at = account.created_at();
        let modified_at = account.modified_at();
        let values = account.into_values();
        Self {
            id: values.id.to_global_id(),
            account_id: UUID::from(values.id),
            version: values.version,
            code: values.code,
            name: values.name,
            normal_balance_type: values.normal_balance_type,
            status: values.status,
            external_id: values.external_id,
            description: values.description,
            metadata: values.metadata.map(JSON::from),
            created_at: created_at.into(),
            modified_at: modified_at.into(),
        }
    }
}

impl From<cala_ledger::account::Account> for AccountCreatePayload {
    fn from(value: cala_ledger::account::Account) -> Self {
        Self {
            account: Account::from(value),
        }
    }
}

impl From<cala_ledger::account::Account> for AccountUpdatePayload {
    fn from(value: cala_ledger::account::Account) -> Self {
        Self {
            account: Account::from(value),
        }
    }
}
