package accounts

import (
	"context"
	"errors"
	"fmt"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/niels/banking-app/backend/internal/domain"
	"github.com/niels/banking-app/backend/internal/integrations/bankledger"
	"github.com/niels/banking-app/backend/internal/routingcodes"
)

type Repository struct {
	db     *pgxpool.Pool
	ledger bankledger.Provider
}

type CreateParams struct {
	UserID            string
	WalletID          string
	AccountNumber     string
	IBAN              string
	BIC               string
	BankProvider      string
	ExternalAccountID string
	Currency          string
}

func NewRepository(db *pgxpool.Pool, providers ...bankledger.Provider) *Repository {
	var provider bankledger.Provider = bankledger.NewLocalProvider()
	if len(providers) > 0 && providers[0] != nil {
		provider = providers[0]
	}
	return &Repository{db: db, ledger: provider}
}

func (r *Repository) issueAccount(ctx context.Context, params CreateParams) (bankledger.IssuedAccount, error) {
	var err error
	issued := bankledger.IssuedAccount{
		Provider:          params.BankProvider,
		ExternalAccountID: params.ExternalAccountID,
		IBAN:              bankledger.NormalizeIBAN(params.IBAN),
		BIC:               bankledger.NormalizeBIC(params.BIC),
	}
	if issued.IBAN == "" || issued.BIC == "" || issued.Provider == "" || issued.ExternalAccountID == "" {
		issued, err = r.ledger.IssueAccount(ctx, bankledger.IssueAccountParams{
			UserID:        params.UserID,
			AccountNumber: params.AccountNumber,
			Currency:      params.Currency,
			Country:       "NL",
		})
		if err != nil {
			return bankledger.IssuedAccount{}, err
		}
	}
	issued.IBAN = bankledger.NormalizeIBAN(issued.IBAN)
	issued.BIC = bankledger.NormalizeBIC(issued.BIC)
	if issued.Provider == "" {
		return bankledger.IssuedAccount{}, fmt.Errorf("%w: bank provider is required", domain.ErrValidation)
	}
	if issued.ExternalAccountID == "" {
		return bankledger.IssuedAccount{}, fmt.Errorf("%w: external account id is required", domain.ErrValidation)
	}
	if err := bankledger.ValidateIBAN(issued.IBAN); err != nil {
		return bankledger.IssuedAccount{}, err
	}
	if err := bankledger.ValidateBIC(issued.BIC); err != nil {
		return bankledger.IssuedAccount{}, err
	}
	issued.RoutingCodes = routingcodes.MergeInferred(issued.RoutingCodes, routingcodes.InferFromIBAN(issued.IBAN))
	issued.RoutingCodes, err = routingcodes.NormalizeAndValidate(issued.RoutingCodes)
	if err != nil {
		return bankledger.IssuedAccount{}, err
	}
	return issued, nil
}

func (r *Repository) Create(ctx context.Context, params CreateParams) (domain.Account, error) {
	issued, err := r.issueAccount(ctx, params)
	if err != nil {
		return domain.Account{}, err
	}

	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return domain.Account{}, err
	}
	defer tx.Rollback(ctx)

	var row pgx.Row
	if params.WalletID != "" {
		row = tx.QueryRow(ctx, `
			INSERT INTO accounts (user_id, wallet_id, account_number, iban, bic, bank_provider, external_account_id, currency)
			SELECT $1, w.id, $3, $4, $5, $6, $7, c.code
			FROM wallets w
			JOIN currencies c ON c.code = $8 AND c.enabled = true
			WHERE w.id = $2 AND w.user_id = $1 AND w.status = 'active'
			RETURNING id::text, user_id::text, COALESCE(wallet_id::text, ''), account_number, iban, bic,
				bank_provider, external_account_id, currency, balance_cents, status, created_at, updated_at
		`, params.UserID, params.WalletID, params.AccountNumber, issued.IBAN, issued.BIC, issued.Provider, issued.ExternalAccountID, params.Currency)
	} else {
		row = tx.QueryRow(ctx, `
			INSERT INTO accounts (user_id, account_number, iban, bic, bank_provider, external_account_id, currency)
			SELECT $1, $2, $3, $4, $5, $6, c.code
			FROM currencies c
			WHERE c.code = $7 AND c.enabled = true
			RETURNING id::text, user_id::text, COALESCE(wallet_id::text, ''), account_number, iban, bic,
				bank_provider, external_account_id, currency, balance_cents, status, created_at, updated_at
		`, params.UserID, params.AccountNumber, issued.IBAN, issued.BIC, issued.Provider, issued.ExternalAccountID, params.Currency)
	}

	account, err := scanAccount(row)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.Account{}, fmt.Errorf("%w: wallet or currency not found", domain.ErrValidation)
		}
		if isUniqueViolation(err) {
			return domain.Account{}, domain.ErrConflict
		}
		return domain.Account{}, err
	}

	if account.WalletID != "" {
		if _, err := tx.Exec(ctx, `
			INSERT INTO wallet_balances (wallet_id, currency)
			VALUES ($1, $2)
			ON CONFLICT (wallet_id, currency) DO NOTHING
		`, account.WalletID, account.Currency); err != nil {
			return domain.Account{}, err
		}
	}

	if err := insertAccountRoutingCodes(ctx, tx, account.ID, issued.RoutingCodes); err != nil {
		return domain.Account{}, err
	}
	account.RoutingCodes, err = listAccountRoutingCodesTx(ctx, tx, account.ID)
	if err != nil {
		return domain.Account{}, err
	}

	if err := tx.Commit(ctx); err != nil {
		return domain.Account{}, err
	}

	return account, nil
}

func (r *Repository) ListByUser(ctx context.Context, userID string) ([]domain.Account, error) {
	rows, err := r.db.Query(ctx, `
		SELECT id::text, user_id::text, COALESCE(wallet_id::text, ''), account_number, iban, bic,
			bank_provider, external_account_id, currency, balance_cents, status, created_at, updated_at
		FROM accounts
		WHERE user_id = $1
		ORDER BY created_at DESC
	`, userID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	accounts := []domain.Account{}
	for rows.Next() {
		account, err := scanAccount(rows)
		if err != nil {
			return nil, err
		}
		accounts = append(accounts, account)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}
	return r.attachRoutingCodes(ctx, accounts)
}

func (r *Repository) FindOwned(ctx context.Context, userID, accountID string) (domain.Account, error) {
	row := r.db.QueryRow(ctx, `
		SELECT id::text, user_id::text, COALESCE(wallet_id::text, ''), account_number, iban, bic,
			bank_provider, external_account_id, currency, balance_cents, status, created_at, updated_at
		FROM accounts
		WHERE id = $1 AND user_id = $2
	`, accountID, userID)

	account, err := scanAccount(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.Account{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.Account{}, err
	}
	accounts, err := r.attachRoutingCodes(ctx, []domain.Account{account})
	if err != nil {
		return domain.Account{}, err
	}
	return accounts[0], nil
}

func insertAccountRoutingCodes(ctx context.Context, tx pgx.Tx, accountID string, codes []domain.RoutingCode) error {
	for _, code := range codes {
		_, err := tx.Exec(ctx, `
			INSERT INTO account_routing_codes (account_id, code_type, country, network, code)
			VALUES ($1, $2, $3, $4, $5)
			ON CONFLICT (account_id, country, network, code_type, code) DO UPDATE
			SET status = 'active',
				updated_at = now()
		`, accountID, code.CodeType, code.Country, code.Network, code.Code)
		if err != nil {
			return err
		}
	}
	return nil
}

func listAccountRoutingCodesTx(ctx context.Context, tx pgx.Tx, accountID string) ([]domain.RoutingCode, error) {
	rows, err := tx.Query(ctx, `
		SELECT id::text, account_id::text, code_type, country::text, network, code, status, created_at, updated_at
		FROM account_routing_codes
		WHERE account_id = $1 AND status = 'active'
		ORDER BY country, network, code_type, code
	`, accountID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	codes := []domain.RoutingCode{}
	for rows.Next() {
		code, err := scanRoutingCode(rows, "account")
		if err != nil {
			return nil, err
		}
		codes = append(codes, code)
	}
	return codes, rows.Err()
}

func (r *Repository) attachRoutingCodes(ctx context.Context, accounts []domain.Account) ([]domain.Account, error) {
	if len(accounts) == 0 {
		return accounts, nil
	}
	ids := make([]string, 0, len(accounts))
	for _, account := range accounts {
		ids = append(ids, account.ID)
	}

	rows, err := r.db.Query(ctx, `
		SELECT id::text, account_id::text, code_type, country::text, network, code, status, created_at, updated_at
		FROM account_routing_codes
		WHERE account_id::text = ANY($1::text[]) AND status = 'active'
		ORDER BY country, network, code_type, code
	`, ids)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	byAccount := map[string][]domain.RoutingCode{}
	for rows.Next() {
		code, err := scanRoutingCode(rows, "account")
		if err != nil {
			return nil, err
		}
		byAccount[code.OwnerID] = append(byAccount[code.OwnerID], code)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	for i := range accounts {
		accounts[i].RoutingCodes = byAccount[accounts[i].ID]
	}
	return accounts, nil
}

type scanner interface {
	Scan(dest ...any) error
}

func scanRoutingCode(row scanner, ownerType string) (domain.RoutingCode, error) {
	var code domain.RoutingCode
	err := row.Scan(
		&code.ID,
		&code.OwnerID,
		&code.CodeType,
		&code.Country,
		&code.Network,
		&code.Code,
		&code.Status,
		&code.CreatedAt,
		&code.UpdatedAt,
	)
	code.OwnerType = ownerType
	return code, err
}

func scanAccount(row scanner) (domain.Account, error) {
	var account domain.Account
	err := row.Scan(
		&account.ID,
		&account.UserID,
		&account.WalletID,
		&account.AccountNumber,
		&account.IBAN,
		&account.BIC,
		&account.BankProvider,
		&account.ExternalAccountID,
		&account.Currency,
		&account.BalanceCents,
		&account.Status,
		&account.CreatedAt,
		&account.UpdatedAt,
	)
	return account, err
}

func isUniqueViolation(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && pgErr.Code == "23505"
}
