package cards

import (
	"context"
	"database/sql"
	"errors"
	"fmt"

	"github.com/jackc/pgx/v5"

	"github.com/niels/banking-app/backend/internal/domain"
	"github.com/niels/banking-app/backend/internal/ledger"
)

func (r *Repository) postAuthorizationHold(ctx context.Context, tx pgx.Tx, auth domain.CardAuthorization, card authCard, account authAccount) (domain.CardAuthorization, error) {
	accountLedger, holdLedger, _, err := r.cardLedgerAccounts(ctx, tx, card, account)
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	entry, err := r.ledger.Post(ctx, tx, ledger.PostParams{
		EventType:      "card_authorization.hold",
		SourceType:     "card_authorization_hold",
		SourceID:       auth.ID,
		IdempotencyKey: "card-hold-" + auth.ID,
		Description:    "Card authorization hold",
		Metadata: map[string]any{
			"virtual_card_id":           card.ID,
			"external_authorization_id": auth.ExternalAuthorizationID,
			"account_id":                account.ID,
		},
		Lines: []ledger.LineParams{
			{LedgerAccountID: accountLedger.ID, Direction: "debit", AmountCents: auth.AmountCents, Currency: auth.Currency},
			{LedgerAccountID: holdLedger.ID, Direction: "credit", AmountCents: auth.AmountCents, Currency: auth.Currency},
		},
	})
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if _, err := recordCardLedgerEvent(ctx, tx, auth.ID, "hold", auth.AmountCents, auth.Currency, entry.ID, ""); err != nil {
		return domain.CardAuthorization{}, err
	}
	return updateAuthorizationLedgerStatus(ctx, tx, auth.ID, "posted")
}

func (r *Repository) clearAuthorization(ctx context.Context, externalAuthorizationID string, amountCents int64, rawEvent []byte) (domain.CardAuthorization, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	defer tx.Rollback(ctx)

	current, card, account, err := lockAuthorizationForLifecycle(ctx, tx, externalAuthorizationID)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.CardAuthorization{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if current.Status == "cleared" {
		return current, tx.Commit(ctx)
	}
	if current.Status != "approved" {
		return domain.CardAuthorization{}, fmt.Errorf("%w: only approved authorizations can be cleared", domain.ErrValidation)
	}
	clearedAmount := amountCents
	if clearedAmount <= 0 {
		clearedAmount = current.AmountCents
	}
	rawJSON := string(redactRawEvent(rawEvent))
	updated, err := scanAuthorization(tx.QueryRow(ctx, `
		UPDATE card_authorizations
		SET status = 'cleared',
			amount_cents = $2,
			cleared_at = now(),
			raw_event = CASE WHEN $3::jsonb = '{}'::jsonb THEN raw_event ELSE $3::jsonb END
		WHERE id = $1
		RETURNING id::text, virtual_card_id::text, external_authorization_id, amount_cents, currency,
			COALESCE(merchant_name, ''), COALESCE(merchant_category_code, ''), COALESCE(merchant_country, ''),
			status, COALESCE(decline_reason, ''), ledger_status,
			expires_at, cleared_at, reversed_at, disputed_at,
			raw_event, created_at, updated_at
	`, current.ID, clearedAmount, rawJSON))
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	updated, err = r.postClearingLedger(ctx, tx, current, updated, card, account)
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if err := insertCardNotification(ctx, tx, card.UserID, card.ID, updated.ID, "", "authorization_cleared", "Card payment cleared", fmt.Sprintf("A card payment for %s %d has cleared.", updated.Currency, updated.AmountCents)); err != nil {
		return domain.CardAuthorization{}, err
	}
	return updated, tx.Commit(ctx)
}

func (r *Repository) postClearingLedger(ctx context.Context, tx pgx.Tx, original, cleared domain.CardAuthorization, card authCard, account authAccount) (domain.CardAuthorization, error) {
	accountLedger, holdLedger, settlementLedger, err := r.cardLedgerAccounts(ctx, tx, card, account)
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	lines := []ledger.LineParams{
		{LedgerAccountID: holdLedger.ID, Direction: "debit", AmountCents: original.AmountCents, Currency: cleared.Currency},
		{LedgerAccountID: settlementLedger.ID, Direction: "credit", AmountCents: cleared.AmountCents, Currency: cleared.Currency},
	}
	if cleared.AmountCents > original.AmountCents {
		lines = append(lines, ledger.LineParams{
			LedgerAccountID: accountLedger.ID,
			Direction:       "debit",
			AmountCents:     cleared.AmountCents - original.AmountCents,
			Currency:        cleared.Currency,
		})
	}
	if original.AmountCents > cleared.AmountCents {
		lines = append(lines, ledger.LineParams{
			LedgerAccountID: accountLedger.ID,
			Direction:       "credit",
			AmountCents:     original.AmountCents - cleared.AmountCents,
			Currency:        cleared.Currency,
		})
	}
	entry, err := r.ledger.Post(ctx, tx, ledger.PostParams{
		EventType:      "card_authorization.cleared",
		SourceType:     "card_authorization_clearing",
		SourceID:       cleared.ID,
		IdempotencyKey: "card-clearing-" + cleared.ID,
		Description:    "Card authorization clearing",
		Metadata: map[string]any{
			"virtual_card_id":           card.ID,
			"external_authorization_id": cleared.ExternalAuthorizationID,
			"original_amount_cents":     original.AmountCents,
			"cleared_amount_cents":      cleared.AmountCents,
		},
		Lines: lines,
	})
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if _, err := recordCardLedgerEvent(ctx, tx, cleared.ID, "clearing", cleared.AmountCents, cleared.Currency, entry.ID, ""); err != nil {
		return domain.CardAuthorization{}, err
	}
	return updateAuthorizationLedgerStatus(ctx, tx, cleared.ID, "posted")
}

func (r *Repository) reverseAuthorization(ctx context.Context, externalAuthorizationID string, rawEvent []byte) (domain.CardAuthorization, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	defer tx.Rollback(ctx)

	current, card, account, err := lockAuthorizationForLifecycle(ctx, tx, externalAuthorizationID)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.CardAuthorization{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if current.Status == "reversed" {
		return current, tx.Commit(ctx)
	}
	if current.Status != "approved" && current.Status != "cleared" && current.Status != "disputed" {
		return domain.CardAuthorization{}, fmt.Errorf("%w: authorization cannot be reversed from status %s", domain.ErrValidation, current.Status)
	}
	rawJSON := string(redactRawEvent(rawEvent))
	updated, err := scanAuthorization(tx.QueryRow(ctx, `
		UPDATE card_authorizations
		SET status = 'reversed',
			reversed_at = now(),
			raw_event = CASE WHEN $2::jsonb = '{}'::jsonb THEN raw_event ELSE $2::jsonb END
		WHERE id = $1
		RETURNING id::text, virtual_card_id::text, external_authorization_id, amount_cents, currency,
			COALESCE(merchant_name, ''), COALESCE(merchant_category_code, ''), COALESCE(merchant_country, ''),
			status, COALESCE(decline_reason, ''), ledger_status,
			expires_at, cleared_at, reversed_at, disputed_at,
			raw_event, created_at, updated_at
	`, current.ID, rawJSON))
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	updated, err = r.postReversalLedger(ctx, tx, current, updated, card, account, "reversal")
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if err := insertCardNotification(ctx, tx, card.UserID, card.ID, updated.ID, "", "authorization_reversed", "Card payment reversed", fmt.Sprintf("A card payment for %s %d was reversed.", updated.Currency, updated.AmountCents)); err != nil {
		return domain.CardAuthorization{}, err
	}
	return updated, tx.Commit(ctx)
}

func (r *Repository) ExpireAuthorizations(ctx context.Context, limit int) ([]domain.CardAuthorization, error) {
	if limit <= 0 || limit > 100 {
		limit = 50
	}
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	rows, err := tx.Query(ctx, `
		SELECT external_authorization_id
		FROM card_authorizations
		WHERE status = 'approved'
			AND expires_at <= now()
		ORDER BY expires_at
		LIMIT $1
		FOR UPDATE SKIP LOCKED
	`, limit)
	if err != nil {
		return nil, err
	}
	ids := []string{}
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			rows.Close()
			return nil, err
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, err
	}
	rows.Close()

	expired := []domain.CardAuthorization{}
	for _, externalID := range ids {
		auth, err := r.expireAuthorizationInTx(ctx, tx, externalID)
		if err != nil {
			return nil, err
		}
		expired = append(expired, auth)
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	return expired, nil
}

func (r *Repository) expireAuthorizationInTx(ctx context.Context, tx pgx.Tx, externalAuthorizationID string) (domain.CardAuthorization, error) {
	current, card, account, err := lockAuthorizationForLifecycle(ctx, tx, externalAuthorizationID)
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if current.Status != "approved" {
		return current, nil
	}
	updated, err := scanAuthorization(tx.QueryRow(ctx, `
		UPDATE card_authorizations
		SET status = 'expired'
		WHERE id = $1
		RETURNING id::text, virtual_card_id::text, external_authorization_id, amount_cents, currency,
			COALESCE(merchant_name, ''), COALESCE(merchant_category_code, ''), COALESCE(merchant_country, ''),
			status, COALESCE(decline_reason, ''), ledger_status,
			expires_at, cleared_at, reversed_at, disputed_at,
			raw_event, created_at, updated_at
	`, current.ID))
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	updated, err = r.postReversalLedger(ctx, tx, current, updated, card, account, "expiry")
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if err := insertCardNotification(ctx, tx, card.UserID, card.ID, updated.ID, "", "authorization_expired", "Card hold expired", fmt.Sprintf("A card hold for %s %d expired.", updated.Currency, updated.AmountCents)); err != nil {
		return domain.CardAuthorization{}, err
	}
	return updated, nil
}

func (r *Repository) postReversalLedger(ctx context.Context, tx pgx.Tx, original, updated domain.CardAuthorization, card authCard, account authAccount, eventType string) (domain.CardAuthorization, error) {
	accountLedger, holdLedger, settlementLedger, err := r.cardLedgerAccounts(ctx, tx, card, account)
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	sourceType := "card_authorization_reversal"
	ledgerEventType := "card_authorization.reversed"
	fromLedger := holdLedger.ID
	if eventType == "expiry" {
		sourceType = "card_authorization_expiry"
		ledgerEventType = "card_authorization.expired"
	} else if original.Status == "cleared" || original.Status == "disputed" {
		fromLedger = settlementLedger.ID
	}
	entry, err := r.ledger.Post(ctx, tx, ledger.PostParams{
		EventType:      ledgerEventType,
		SourceType:     sourceType,
		SourceID:       updated.ID,
		IdempotencyKey: "card-" + eventType + "-" + updated.ID,
		Description:    "Card authorization " + eventType,
		Metadata: map[string]any{
			"virtual_card_id":           card.ID,
			"external_authorization_id": updated.ExternalAuthorizationID,
			"previous_status":           original.Status,
		},
		Lines: []ledger.LineParams{
			{LedgerAccountID: fromLedger, Direction: "debit", AmountCents: original.AmountCents, Currency: original.Currency},
			{LedgerAccountID: accountLedger.ID, Direction: "credit", AmountCents: original.AmountCents, Currency: original.Currency},
		},
	})
	if err != nil {
		return domain.CardAuthorization{}, err
	}
	if _, err := recordCardLedgerEvent(ctx, tx, updated.ID, eventType, original.AmountCents, original.Currency, entry.ID, ""); err != nil {
		return domain.CardAuthorization{}, err
	}
	return updateAuthorizationLedgerStatus(ctx, tx, updated.ID, "posted")
}

func (r *Repository) cardLedgerAccounts(ctx context.Context, tx pgx.Tx, card authCard, account authAccount) (domain.LedgerAccount, domain.LedgerAccount, domain.LedgerAccount, error) {
	accountLedger, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		OwnerUserID:   card.UserID,
		ReferenceType: "account",
		ReferenceID:   account.ID,
		Currency:      account.Currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return domain.LedgerAccount{}, domain.LedgerAccount{}, domain.LedgerAccount{}, err
	}
	holdLedger, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		OwnerUserID:   card.UserID,
		ReferenceType: "card_authorization_hold",
		ReferenceID:   card.ID,
		Currency:      account.Currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return domain.LedgerAccount{}, domain.LedgerAccount{}, domain.LedgerAccount{}, err
	}
	settlementLedger, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		ReferenceType: "card_issuer_settlement",
		ReferenceID:   "outbound_card_clearing",
		Currency:      account.Currency,
		NormalBalance: "credit",
	})
	return accountLedger, holdLedger, settlementLedger, err
}

func lockAuthorizationForLifecycle(ctx context.Context, tx pgx.Tx, externalAuthorizationID string) (domain.CardAuthorization, authCard, authAccount, error) {
	row := tx.QueryRow(ctx, `
		SELECT ca.id::text, ca.virtual_card_id::text, ca.external_authorization_id, ca.amount_cents, ca.currency,
			COALESCE(ca.merchant_name, ''), COALESCE(ca.merchant_category_code, ''), COALESCE(ca.merchant_country, ''),
			ca.status, COALESCE(ca.decline_reason, ''), ca.ledger_status,
			ca.expires_at, ca.cleared_at, ca.reversed_at, ca.disputed_at,
			ca.raw_event, ca.created_at, ca.updated_at,
			vc.id::text, vc.user_id::text, vc.status, vc.spending_limit_cents,
			COALESCE(kp.status, ''), EXISTS (
				SELECT 1 FROM aml_cases ac
				WHERE ac.user_id = vc.user_id AND ac.status IN ('open', 'reviewing', 'escalated')
			),
			a.id::text, a.balance_cents, a.currency, a.status
		FROM card_authorizations ca
		JOIN virtual_cards vc ON vc.id = ca.virtual_card_id
		JOIN accounts a ON a.id = vc.account_id
		LEFT JOIN kyc_profiles kp ON kp.user_id = vc.user_id
		WHERE ca.external_authorization_id = $1
		FOR UPDATE OF ca, vc, a
	`, externalAuthorizationID)

	var auth domain.CardAuthorization
	var card authCard
	var account authAccount
	var expiresAt sql.NullTime
	var clearedAt sql.NullTime
	var reversedAt sql.NullTime
	var disputedAt sql.NullTime
	err := row.Scan(
		&auth.ID,
		&auth.VirtualCardID,
		&auth.ExternalAuthorizationID,
		&auth.AmountCents,
		&auth.Currency,
		&auth.MerchantName,
		&auth.MerchantCategoryCode,
		&auth.MerchantCountry,
		&auth.Status,
		&auth.DeclineReason,
		&auth.LedgerStatus,
		&expiresAt,
		&clearedAt,
		&reversedAt,
		&disputedAt,
		&auth.RawEvent,
		&auth.CreatedAt,
		&auth.UpdatedAt,
		&card.ID,
		&card.UserID,
		&card.Status,
		&card.SpendingLimitCents,
		&card.KYCStatus,
		&card.HasOpenAMLCase,
		&account.ID,
		&account.BalanceCents,
		&account.Currency,
		&account.Status,
	)
	if expiresAt.Valid {
		auth.ExpiresAt = &expiresAt.Time
	}
	if clearedAt.Valid {
		auth.ClearedAt = &clearedAt.Time
	}
	if reversedAt.Valid {
		auth.ReversedAt = &reversedAt.Time
	}
	if disputedAt.Valid {
		auth.DisputedAt = &disputedAt.Time
	}
	return auth, card, account, err
}

func updateAuthorizationLedgerStatus(ctx context.Context, tx pgx.Tx, authorizationID, status string) (domain.CardAuthorization, error) {
	return scanAuthorization(tx.QueryRow(ctx, `
		UPDATE card_authorizations
		SET ledger_status = $2
		WHERE id = $1
		RETURNING id::text, virtual_card_id::text, external_authorization_id, amount_cents, currency,
			COALESCE(merchant_name, ''), COALESCE(merchant_category_code, ''), COALESCE(merchant_country, ''),
			status, COALESCE(decline_reason, ''), ledger_status,
			expires_at, cleared_at, reversed_at, disputed_at,
			raw_event, created_at, updated_at
	`, authorizationID, status))
}

func recordCardLedgerEvent(ctx context.Context, tx pgx.Tx, authorizationID, eventType string, amountCents int64, currency, journalEntryID, reason string) (string, error) {
	var id string
	err := tx.QueryRow(ctx, `
		INSERT INTO card_authorization_ledger_events (
			card_authorization_id, event_type, amount_cents, currency, ledger_journal_entry_id, status, reason
		)
		VALUES ($1, $2, $3, $4, NULLIF($5, '')::uuid, 'posted', NULLIF($6, ''))
		ON CONFLICT (card_authorization_id, event_type) DO UPDATE
		SET ledger_journal_entry_id = COALESCE(EXCLUDED.ledger_journal_entry_id, card_authorization_ledger_events.ledger_journal_entry_id)
		RETURNING id::text
	`, authorizationID, eventType, amountCents, currency, journalEntryID, reason).Scan(&id)
	return id, err
}

func insertCardNotification(ctx context.Context, tx pgx.Tx, userID, cardID, authorizationID, disputeID, notificationType, title, body string) error {
	_, err := tx.Exec(ctx, `
		INSERT INTO cardholder_notifications (
			user_id, virtual_card_id, card_authorization_id, card_dispute_id,
			notification_type, channel, status, title, body, sent_at
		)
		VALUES ($1, NULLIF($2, '')::uuid, NULLIF($3, '')::uuid, NULLIF($4, '')::uuid,
			$5, 'in_app', 'sent', $6, $7, now())
	`, userID, cardID, authorizationID, disputeID, notificationType, title, body)
	return err
}

func notificationTypeForAuthorization(auth domain.CardAuthorization) string {
	if auth.Status == "approved" {
		return "authorization_approved"
	}
	return "authorization_declined"
}

func cardNotificationTitle(auth domain.CardAuthorization) string {
	if auth.Status == "approved" {
		return "Card payment approved"
	}
	return "Card payment declined"
}

func cardNotificationBody(auth domain.CardAuthorization) string {
	if auth.Status == "approved" {
		return fmt.Sprintf("A card payment for %s %d was approved.", auth.Currency, auth.AmountCents)
	}
	if auth.DeclineReason != "" {
		return "A card payment was declined: " + auth.DeclineReason + "."
	}
	return "A card payment was declined."
}
