package beneficiaries

import (
	"context"
	"errors"

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

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

type Repository struct {
	db *pgxpool.Pool
}

type CreateParams struct {
	UserID       string
	Name         string
	IBAN         string
	BIC          string
	Currency     string
	RoutingCodes []domain.RoutingCode
}

func NewRepository(db *pgxpool.Pool) *Repository {
	return &Repository{db: db}
}

func (r *Repository) Create(ctx context.Context, params CreateParams) (domain.Beneficiary, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{})
	if err != nil {
		return domain.Beneficiary{}, err
	}
	defer tx.Rollback(ctx)

	row := tx.QueryRow(ctx, `
		INSERT INTO beneficiaries (user_id, name, iban, bic, currency)
		SELECT $1, $2, $3, NULLIF($4, ''), c.code
		FROM currencies c
		WHERE c.code = $5 AND c.enabled = true
		ON CONFLICT (user_id, iban) DO UPDATE
		SET name = EXCLUDED.name,
			bic = EXCLUDED.bic,
			currency = EXCLUDED.currency,
			status = 'active',
			updated_at = now()
		RETURNING id::text, user_id::text, name, iban, COALESCE(bic, ''), currency, status, created_at, updated_at
	`, params.UserID, params.Name, params.IBAN, params.BIC, params.Currency)

	beneficiary, err := scanBeneficiary(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.Beneficiary{}, domain.ErrValidation
	}
	if err != nil {
		return domain.Beneficiary{}, err
	}
	if err := replaceBeneficiaryRoutingCodes(ctx, tx, beneficiary.ID, params.RoutingCodes); err != nil {
		return domain.Beneficiary{}, err
	}
	beneficiary.RoutingCodes, err = listBeneficiaryRoutingCodesTx(ctx, tx, beneficiary.ID)
	if err != nil {
		return domain.Beneficiary{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return domain.Beneficiary{}, err
	}
	return beneficiary, nil
}

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

	beneficiaries := []domain.Beneficiary{}
	for rows.Next() {
		beneficiary, err := scanBeneficiary(rows)
		if err != nil {
			return nil, err
		}
		beneficiaries = append(beneficiaries, beneficiary)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return r.attachRoutingCodes(ctx, beneficiaries)
}

func (r *Repository) Delete(ctx context.Context, userID, beneficiaryID string) error {
	tag, err := r.db.Exec(ctx, `
		UPDATE beneficiaries
		SET status = 'deleted'
		WHERE id = $1 AND user_id = $2 AND status = 'active'
	`, beneficiaryID, userID)
	if err != nil {
		return err
	}
	if tag.RowsAffected() == 0 {
		return domain.ErrNotFound
	}
	return nil
}

func replaceBeneficiaryRoutingCodes(ctx context.Context, tx pgx.Tx, beneficiaryID string, codes []domain.RoutingCode) error {
	if _, err := tx.Exec(ctx, `DELETE FROM beneficiary_routing_codes WHERE beneficiary_id = $1`, beneficiaryID); err != nil {
		return err
	}
	for _, code := range codes {
		if _, err := tx.Exec(ctx, `
			INSERT INTO beneficiary_routing_codes (beneficiary_id, code_type, country, network, code)
			VALUES ($1, $2, $3, $4, $5)
		`, beneficiaryID, code.CodeType, code.Country, code.Network, code.Code); err != nil {
			return err
		}
	}
	return nil
}

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

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

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

	rows, err := r.db.Query(ctx, `
		SELECT id::text, beneficiary_id::text, code_type, country::text, network, code, status, created_at, updated_at
		FROM beneficiary_routing_codes
		WHERE beneficiary_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()

	byBeneficiary := map[string][]domain.RoutingCode{}
	for rows.Next() {
		code, err := scanRoutingCode(rows, "beneficiary")
		if err != nil {
			return nil, err
		}
		byBeneficiary[code.OwnerID] = append(byBeneficiary[code.OwnerID], code)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	for i := range beneficiaries {
		beneficiaries[i].RoutingCodes = byBeneficiary[beneficiaries[i].ID]
	}
	return beneficiaries, 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 scanBeneficiary(row scanner) (domain.Beneficiary, error) {
	var beneficiary domain.Beneficiary
	err := row.Scan(
		&beneficiary.ID,
		&beneficiary.UserID,
		&beneficiary.Name,
		&beneficiary.IBAN,
		&beneficiary.BIC,
		&beneficiary.Currency,
		&beneficiary.Status,
		&beneficiary.CreatedAt,
		&beneficiary.UpdatedAt,
	)
	return beneficiary, err
}
