package ledger

import (
	"context"
	"encoding/json"
	"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"
)

type Repository struct {
	db *pgxpool.Pool
}

type AccountParams struct {
	OwnerUserID   string
	ReferenceType string
	ReferenceID   string
	Currency      string
	NormalBalance string
}

type PostParams struct {
	EventType      string
	SourceType     string
	SourceID       string
	IdempotencyKey string
	Description    string
	Metadata       any
	Lines          []LineParams
}

type LineParams struct {
	LedgerAccountID string
	Direction       string
	AmountCents     int64
	Currency        string
}

type txer interface {
	Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
	Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

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

func (r *Repository) EnsureAccount(ctx context.Context, tx txer, params AccountParams) (domain.LedgerAccount, error) {
	if params.ReferenceType == "" || params.ReferenceID == "" {
		return domain.LedgerAccount{}, fmt.Errorf("%w: ledger account reference is required", domain.ErrValidation)
	}
	if err := domain.ValidateCurrency(params.Currency); err != nil {
		return domain.LedgerAccount{}, err
	}
	if params.NormalBalance != "debit" && params.NormalBalance != "credit" {
		return domain.LedgerAccount{}, fmt.Errorf("%w: normal_balance must be debit or credit", domain.ErrValidation)
	}

	row := tx.QueryRow(ctx, `
		INSERT INTO ledger_accounts (owner_user_id, reference_type, reference_id, currency, normal_balance)
		VALUES (NULLIF($1, '')::uuid, $2, $3, $4, $5)
		ON CONFLICT (reference_type, reference_id, currency) DO UPDATE
		SET status = ledger_accounts.status
		RETURNING id::text, COALESCE(owner_user_id::text, ''), reference_type, reference_id,
			currency, normal_balance, status, created_at, updated_at
	`, params.OwnerUserID, params.ReferenceType, params.ReferenceID, params.Currency, params.NormalBalance)

	return scanAccount(row)
}

func (r *Repository) Post(ctx context.Context, tx txer, params PostParams) (domain.LedgerJournalEntry, error) {
	if err := validatePost(params); err != nil {
		return domain.LedgerJournalEntry{}, err
	}

	metadata, err := marshalMetadata(params.Metadata)
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}

	row := tx.QueryRow(ctx, `
		INSERT INTO ledger_journal_entries (event_type, source_type, source_id, idempotency_key, description, metadata)
		VALUES ($1, $2, $3, NULLIF($4, ''), NULLIF($5, ''), $6)
		ON CONFLICT (source_type, source_id) DO NOTHING
		RETURNING id::text, event_type, source_type, source_id, COALESCE(idempotency_key, ''),
			COALESCE(description, ''), metadata, created_at
	`, params.EventType, params.SourceType, params.SourceID, params.IdempotencyKey, params.Description, metadata)

	entry, err := scanEntry(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return r.FindBySource(ctx, tx, params.SourceType, params.SourceID)
	}
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}

	for _, line := range params.Lines {
		if _, err := tx.Exec(ctx, `
			INSERT INTO ledger_journal_lines (journal_entry_id, ledger_account_id, direction, amount_cents, currency)
			VALUES ($1, $2, $3, $4, $5)
		`, entry.ID, line.LedgerAccountID, line.Direction, line.AmountCents, line.Currency); err != nil {
			return domain.LedgerJournalEntry{}, err
		}
	}

	entry.Lines, err = r.listLines(ctx, tx, entry.ID)
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}
	return entry, nil
}

func (r *Repository) FindBySource(ctx context.Context, tx txer, sourceType, sourceID string) (domain.LedgerJournalEntry, error) {
	row := tx.QueryRow(ctx, `
		SELECT id::text, event_type, source_type, source_id, COALESCE(idempotency_key, ''),
			COALESCE(description, ''), metadata, created_at
		FROM ledger_journal_entries
		WHERE source_type = $1 AND source_id = $2
	`, sourceType, sourceID)

	entry, err := scanEntry(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.LedgerJournalEntry{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}

	entry.Lines, err = r.listLines(ctx, tx, entry.ID)
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}
	return entry, nil
}

func (r *Repository) ListJournalEntries(ctx context.Context, limit int) ([]domain.LedgerJournalEntry, error) {
	if limit <= 0 || limit > 100 {
		limit = 50
	}

	rows, err := r.db.Query(ctx, `
		SELECT id::text, event_type, source_type, source_id, COALESCE(idempotency_key, ''),
			COALESCE(description, ''), metadata, created_at
		FROM ledger_journal_entries
		ORDER BY created_at DESC
		LIMIT $1
	`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	entries := []domain.LedgerJournalEntry{}
	for rows.Next() {
		entry, err := scanEntry(rows)
		if err != nil {
			return nil, err
		}
		entry.Lines, err = r.listLines(ctx, r.db, entry.ID)
		if err != nil {
			return nil, err
		}
		entries = append(entries, entry)
	}
	return entries, rows.Err()
}

func (r *Repository) FindJournalEntry(ctx context.Context, entryID string) (domain.LedgerJournalEntry, error) {
	row := r.db.QueryRow(ctx, `
		SELECT id::text, event_type, source_type, source_id, COALESCE(idempotency_key, ''),
			COALESCE(description, ''), metadata, created_at
		FROM ledger_journal_entries
		WHERE id = $1
	`, entryID)

	entry, err := scanEntry(row)
	if errors.Is(err, pgx.ErrNoRows) {
		return domain.LedgerJournalEntry{}, domain.ErrNotFound
	}
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}

	entry.Lines, err = r.listLines(ctx, r.db, entry.ID)
	if err != nil {
		return domain.LedgerJournalEntry{}, err
	}
	return entry, nil
}

func (r *Repository) listLines(ctx context.Context, q txer, entryID string) ([]domain.LedgerJournalLine, error) {
	rows, err := q.Query(ctx, `
		SELECT jl.id::text, jl.journal_entry_id::text, jl.ledger_account_id::text,
			la.reference_type, la.reference_id, jl.direction, jl.amount_cents, jl.currency, jl.created_at
		FROM ledger_journal_lines jl
		JOIN ledger_accounts la ON la.id = jl.ledger_account_id
		WHERE jl.journal_entry_id = $1
		ORDER BY jl.created_at, jl.id
	`, entryID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	lines := []domain.LedgerJournalLine{}
	for rows.Next() {
		line, err := scanLine(rows)
		if err != nil {
			return nil, err
		}
		lines = append(lines, line)
	}
	return lines, rows.Err()
}

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

func scanAccount(row scanner) (domain.LedgerAccount, error) {
	var account domain.LedgerAccount
	err := row.Scan(
		&account.ID,
		&account.OwnerUserID,
		&account.ReferenceType,
		&account.ReferenceID,
		&account.Currency,
		&account.NormalBalance,
		&account.Status,
		&account.CreatedAt,
		&account.UpdatedAt,
	)
	return account, err
}

func scanEntry(row scanner) (domain.LedgerJournalEntry, error) {
	var entry domain.LedgerJournalEntry
	err := row.Scan(
		&entry.ID,
		&entry.EventType,
		&entry.SourceType,
		&entry.SourceID,
		&entry.IdempotencyKey,
		&entry.Description,
		&entry.Metadata,
		&entry.CreatedAt,
	)
	return entry, err
}

func scanLine(row scanner) (domain.LedgerJournalLine, error) {
	var line domain.LedgerJournalLine
	err := row.Scan(
		&line.ID,
		&line.JournalEntryID,
		&line.LedgerAccountID,
		&line.LedgerAccountReferenceType,
		&line.LedgerAccountReferenceID,
		&line.Direction,
		&line.AmountCents,
		&line.Currency,
		&line.CreatedAt,
	)
	return line, err
}

func validatePost(params PostParams) error {
	if params.EventType == "" || params.SourceType == "" || params.SourceID == "" {
		return fmt.Errorf("%w: ledger event_type, source_type and source_id are required", domain.ErrValidation)
	}
	if len(params.Lines) < 2 {
		return fmt.Errorf("%w: ledger posting requires at least two lines", domain.ErrValidation)
	}

	totals := map[string]struct {
		debit  int64
		credit int64
	}{}
	for _, line := range params.Lines {
		if line.LedgerAccountID == "" {
			return fmt.Errorf("%w: ledger_account_id is required", domain.ErrValidation)
		}
		if line.Direction != "debit" && line.Direction != "credit" {
			return fmt.Errorf("%w: ledger line direction must be debit or credit", domain.ErrValidation)
		}
		if err := domain.ValidateAmount(line.AmountCents); err != nil {
			return err
		}
		if err := domain.ValidateCurrency(line.Currency); err != nil {
			return err
		}

		total := totals[line.Currency]
		if line.Direction == "debit" {
			total.debit += line.AmountCents
		} else {
			total.credit += line.AmountCents
		}
		totals[line.Currency] = total
	}

	for currency, total := range totals {
		if total.debit != total.credit {
			return fmt.Errorf("%w: ledger posting is not balanced for %s", domain.ErrValidation, currency)
		}
	}
	return nil
}

func marshalMetadata(metadata any) ([]byte, error) {
	if metadata == nil {
		return []byte("{}"), nil
	}
	raw, err := json.Marshal(metadata)
	if err != nil {
		return nil, fmt.Errorf("%w: ledger metadata must be JSON serializable", domain.ErrValidation)
	}
	return raw, nil
}
