package settlement

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"time"

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

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

const localSEPAProvider = "local_sepa"

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

type Worker struct {
	repo        *Repository
	enabled     bool
	interval    time.Duration
	batchSize   int
	maxAttempts int
	retryDelay  time.Duration
	log         *slog.Logger
}

type dueTransfer struct {
	ID               string
	UserID           string
	FromAccountID    string
	WalletID         string
	AmountCents      int64
	Currency         string
	Status           string
	Attempts         int
	BeneficiaryIBAN  string
	PaymentReference string
	Description      string
}

type settlementPolicy struct {
	MaxAttempts int
	RetryDelay  time.Duration
}

type providerOutcome string

const (
	providerSucceeded        providerOutcome = "succeeded"
	providerTemporaryFailure providerOutcome = "temporary_failure"
	providerPermanentFailure providerOutcome = "permanent_failure"
)

type providerDecision struct {
	Outcome providerOutcome
	Code    string
	Reason  string
}

func NewRepository(db *pgxpool.Pool, ledgers ...*ledger.Repository) *Repository {
	ledgerRepo := ledger.NewRepository(db)
	if len(ledgers) > 0 && ledgers[0] != nil {
		ledgerRepo = ledgers[0]
	}
	return &Repository{db: db, ledger: ledgerRepo}
}

func NewWorker(repo *Repository, cfg config.SettlementConfig, log *slog.Logger) *Worker {
	return &Worker{
		repo:        repo,
		enabled:     cfg.SEPAEnabled,
		interval:    cfg.Interval,
		batchSize:   cfg.BatchSize,
		maxAttempts: cfg.MaxAttempts,
		retryDelay:  cfg.RetryDelay,
		log:         log,
	}
}

func (w *Worker) Run(ctx context.Context) {
	if !w.enabled {
		if w.log != nil {
			w.log.Info("SEPA settlement worker disabled")
		}
		return
	}

	w.process(ctx)
	ticker := time.NewTicker(w.interval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			if w.log != nil {
				w.log.Info("SEPA settlement worker stopped")
			}
			return
		case <-ticker.C:
			w.process(ctx)
		}
	}
}

func (w *Worker) process(ctx context.Context) {
	processed, err := w.repo.ProcessDue(ctx, w.batchSize, settlementPolicy{
		MaxAttempts: w.maxAttempts,
		RetryDelay:  w.retryDelay,
	})
	if err != nil {
		if w.log != nil && !errors.Is(err, context.Canceled) {
			w.log.Error("SEPA settlement batch failed", "error", err)
		}
		return
	}
	if processed > 0 && w.log != nil {
		w.log.Info("SEPA settlement batch processed", "count", processed)
	}
}

func (r *Repository) ProcessDue(ctx context.Context, limit int, policy settlementPolicy) (int, error) {
	if limit <= 0 || limit > 100 {
		limit = 25
	}
	if policy.MaxAttempts <= 0 {
		policy.MaxAttempts = 3
	}
	if policy.RetryDelay < time.Second {
		policy.RetryDelay = time.Minute
	}

	processed := 0
	for processed < limit {
		ok, err := r.processOne(ctx, policy)
		if err != nil {
			return processed, err
		}
		if !ok {
			break
		}
		processed++
	}
	return processed, nil
}

func (r *Repository) processOne(ctx context.Context, policy settlementPolicy) (bool, error) {
	tx, err := r.db.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.ReadCommitted})
	if err != nil {
		return false, err
	}
	defer tx.Rollback(ctx)

	transfer, err := selectDueTransfer(ctx, tx)
	if errors.Is(err, pgx.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}

	attempt := transfer.Attempts + 1
	providerReference := settlementReference(transfer.ID, attempt)
	if err := payments.RequireTransition(payments.StatusPending, payments.StatusProcessing); err != nil {
		return false, err
	}
	processingReason := payments.ReasonMapping{
		Provider:     localSEPAProvider,
		ProviderCode: "SUBMITTED",
		InternalCode: "settlement_submitted",
		Category:     "system",
		Severity:     "low",
		Description:  "Settlement submitted to local SEPA rail",
	}
	if _, err := tx.Exec(ctx, `
		UPDATE transfers
		SET status = 'processing',
			settlement_attempts = settlement_attempts + 1,
			settlement_provider = $2,
			settlement_reference = $3,
			settlement_error = NULL,
			settlement_next_attempt_at = NULL,
			payment_reason_code = $4,
			payment_reason_category = $5,
			payment_reason_description = $6,
			manual_review_required = false
		WHERE id = $1
	`, transfer.ID, localSEPAProvider, providerReference, processingReason.InternalCode, processingReason.Category, processingReason.Description); err != nil {
		return false, err
	}

	if err := insertEvent(ctx, tx, transfer.ID, "processing", "pending", "processing", providerReference, "Settlement submitted to local SEPA rail"); err != nil {
		return false, err
	}
	if err := payments.RecordStatusEvent(ctx, tx, payments.StatusEventParams{
		TransferID:   transfer.ID,
		FromStatus:   payments.StatusPending,
		ToStatus:     payments.StatusProcessing,
		Provider:     localSEPAProvider,
		ProviderCode: processingReason.ProviderCode,
		Reason:       processingReason,
		Note:         processingReason.Description,
		ActorType:    payments.ActorProvider,
	}); err != nil {
		return false, err
	}

	decision := localProviderDecision(transfer, attempt)
	switch {
	case decision.Outcome == providerSucceeded:
		if err := r.completeSettlement(ctx, tx, transfer, providerReference, decision); err != nil {
			return false, err
		}
	case decision.Outcome == providerTemporaryFailure && attempt < policy.MaxAttempts:
		if err := retrySettlement(ctx, tx, transfer, providerReference, decision, policy.RetryDelay); err != nil {
			return false, err
		}
	default:
		failDecision := decision
		if decision.Outcome == providerTemporaryFailure {
			failDecision.Code = payments.ProviderCodeMaxAttemptsExceeded
			failDecision.Reason = fmt.Sprintf("%s after %d attempts", decision.Reason, attempt)
		}
		if err := r.failAndRefund(ctx, tx, transfer, providerReference, failDecision); err != nil {
			return false, err
		}
	}

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

func (r *Repository) completeSettlement(ctx context.Context, tx pgx.Tx, transfer dueTransfer, providerReference string, decision providerDecision) error {
	if err := r.postSettlementLedger(ctx, tx, transfer, providerReference); err != nil {
		return err
	}
	reason, err := payments.MapReason(ctx, tx, localSEPAProvider, decision.Code)
	if err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE transfers
		SET status = 'completed',
			settlement_error = NULL,
			settlement_next_attempt_at = NULL,
			settled_at = now(),
			payment_reason_code = $2,
			payment_reason_category = $3,
			payment_reason_description = $4,
			manual_review_required = false
		WHERE id = $1
	`, transfer.ID, reason.InternalCode, reason.Category, reason.Description); err != nil {
		return err
	}

	if err := insertEvent(ctx, tx, transfer.ID, "completed", "processing", "completed", providerReference, "Settlement completed"); err != nil {
		return err
	}
	if err := payments.RecordStatusEvent(ctx, tx, payments.StatusEventParams{
		TransferID:   transfer.ID,
		FromStatus:   payments.StatusProcessing,
		ToStatus:     payments.StatusCompleted,
		Provider:     localSEPAProvider,
		ProviderCode: decision.Code,
		Reason:       reason,
		Note:         decision.Reason,
		ActorType:    payments.ActorProvider,
	}); err != nil {
		return err
	}
	if err := payments.RecordNotification(ctx, tx, payments.NotificationParams{
		UserID:     transfer.UserID,
		TransferID: transfer.ID,
		Type:       "payment.completed",
		Title:      "SEPA payment completed",
		Message:    "Your SEPA payment has been completed.",
		Payload: map[string]any{
			"transfer_id":        transfer.ID,
			"provider_reference": providerReference,
		},
	}); err != nil {
		return err
	}
	return nil
}

func selectDueTransfer(ctx context.Context, tx pgx.Tx) (dueTransfer, error) {
	var transfer dueTransfer
	err := tx.QueryRow(ctx, `
		SELECT t.id::text, t.user_id::text, t.from_account_id::text, COALESCE(a.wallet_id::text, ''),
			t.amount_cents, t.currency, t.status, t.settlement_attempts, COALESCE(t.beneficiary_iban, ''),
			COALESCE(t.payment_reference, ''), COALESCE(t.description, '')
		FROM transfers t
		JOIN accounts a ON a.id = t.from_account_id
		WHERE t.transfer_type = 'sepa'
			AND t.status = 'pending'
			AND (t.settlement_next_attempt_at IS NULL OR t.settlement_next_attempt_at <= now())
		ORDER BY t.created_at ASC
		LIMIT 1
		FOR UPDATE OF t SKIP LOCKED
	`).Scan(
		&transfer.ID,
		&transfer.UserID,
		&transfer.FromAccountID,
		&transfer.WalletID,
		&transfer.AmountCents,
		&transfer.Currency,
		&transfer.Status,
		&transfer.Attempts,
		&transfer.BeneficiaryIBAN,
		&transfer.PaymentReference,
		&transfer.Description,
	)
	return transfer, err
}

func retrySettlement(ctx context.Context, tx pgx.Tx, transfer dueTransfer, providerReference string, decision providerDecision, delay time.Duration) error {
	reason, err := payments.MapReason(ctx, tx, localSEPAProvider, decision.Code)
	if err != nil {
		return err
	}
	nextAttemptAt := time.Now().UTC().Add(delay)
	if _, err := tx.Exec(ctx, `
		UPDATE transfers
		SET status = 'pending',
			settlement_error = $2,
			settlement_next_attempt_at = $3,
			payment_reason_code = $4,
			payment_reason_category = $5,
			payment_reason_description = $6,
			manual_review_required = false
		WHERE id = $1
	`, transfer.ID, decision.Reason, nextAttemptAt, reason.InternalCode, reason.Category, reason.Description); err != nil {
		return err
	}

	if err := insertEvent(ctx, tx, transfer.ID, "retried", "processing", "pending", providerReference, "Temporary settlement failure, retry scheduled: "+decision.Reason); err != nil {
		return err
	}
	if err := payments.RecordStatusEvent(ctx, tx, payments.StatusEventParams{
		TransferID:   transfer.ID,
		FromStatus:   payments.StatusProcessing,
		ToStatus:     payments.StatusPending,
		Provider:     localSEPAProvider,
		ProviderCode: decision.Code,
		Reason:       reason,
		Note:         decision.Reason,
		ActorType:    payments.ActorProvider,
	}); err != nil {
		return err
	}
	return payments.RecordNotification(ctx, tx, payments.NotificationParams{
		UserID:     transfer.UserID,
		TransferID: transfer.ID,
		Type:       "payment.retry_scheduled",
		Title:      "SEPA payment retry scheduled",
		Message:    "Your SEPA payment is being retried after a temporary provider issue.",
		Payload: map[string]any{
			"transfer_id":     transfer.ID,
			"next_attempt_at": nextAttemptAt,
			"reason":          decision.Reason,
		},
	})
}

func (r *Repository) failAndRefund(ctx context.Context, tx pgx.Tx, transfer dueTransfer, providerReference string, decision providerDecision) error {
	if _, err := tx.Exec(ctx, `
		UPDATE accounts
		SET balance_cents = balance_cents + $1, updated_at = now()
		WHERE id = $2
	`, transfer.AmountCents, transfer.FromAccountID); err != nil {
		return err
	}

	if transfer.WalletID != "" {
		if _, err := tx.Exec(ctx, `
			INSERT INTO wallet_balances (wallet_id, currency)
			VALUES ($1, $2)
			ON CONFLICT (wallet_id, currency) DO NOTHING
		`, transfer.WalletID, transfer.Currency); err != nil {
			return err
		}
		if _, err := tx.Exec(ctx, `
			UPDATE wallet_balances
			SET available_balance_cents = available_balance_cents + $1
			WHERE wallet_id = $2 AND currency = $3
		`, transfer.AmountCents, transfer.WalletID, transfer.Currency); err != nil {
			return err
		}
	}

	if err := r.postRefundLedger(ctx, tx, transfer, providerReference, decision.Reason); err != nil {
		return err
	}

	reason, err := payments.MapReason(ctx, tx, localSEPAProvider, decision.Code)
	if err != nil {
		return err
	}
	if _, err := tx.Exec(ctx, `
		UPDATE transfers
		SET status = 'failed',
			settlement_error = $2,
			settlement_next_attempt_at = NULL,
			settled_at = NULL,
			payment_reason_code = $3,
			payment_reason_category = $4,
			payment_reason_description = $5,
			manual_review_required = false
		WHERE id = $1
	`, transfer.ID, decision.Reason, reason.InternalCode, reason.Category, reason.Description); err != nil {
		return err
	}

	if err := insertEvent(ctx, tx, transfer.ID, "failed", "processing", "failed", providerReference, "Settlement failed and customer funds refunded: "+decision.Reason); err != nil {
		return err
	}
	if err := payments.RecordStatusEvent(ctx, tx, payments.StatusEventParams{
		TransferID:   transfer.ID,
		FromStatus:   payments.StatusProcessing,
		ToStatus:     payments.StatusFailed,
		Provider:     localSEPAProvider,
		ProviderCode: decision.Code,
		Reason:       reason,
		Note:         decision.Reason,
		ActorType:    payments.ActorProvider,
	}); err != nil {
		return err
	}
	return payments.RecordNotification(ctx, tx, payments.NotificationParams{
		UserID:     transfer.UserID,
		TransferID: transfer.ID,
		Type:       "payment.failed_refunded",
		Title:      "SEPA payment failed",
		Message:    "Your SEPA payment failed and the funds were returned to your account.",
		Payload: map[string]any{
			"transfer_id": transfer.ID,
			"reason":      decision.Reason,
		},
	})
}

func (r *Repository) postSettlementLedger(ctx context.Context, tx pgx.Tx, transfer dueTransfer, providerReference string) error {
	clearing, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		ReferenceType: "external_clearing",
		ReferenceID:   "sepa_outbound",
		Currency:      transfer.Currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return err
	}
	settlementCash, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		ReferenceType: "settlement_cash",
		ReferenceID:   "sepa_outbound",
		Currency:      transfer.Currency,
		NormalBalance: "debit",
	})
	if err != nil {
		return err
	}

	_, err = r.ledger.Post(ctx, tx, ledger.PostParams{
		EventType:      "sepa.settlement.completed",
		SourceType:     "sepa_settlement",
		SourceID:       transfer.ID,
		IdempotencyKey: "sepa-settlement-" + transfer.ID,
		Description:    "SEPA outbound settlement completed",
		Metadata: map[string]any{
			"transfer_id":        transfer.ID,
			"provider":           localSEPAProvider,
			"provider_reference": providerReference,
		},
		Lines: []ledger.LineParams{
			{
				LedgerAccountID: clearing.ID,
				Direction:       "debit",
				AmountCents:     transfer.AmountCents,
				Currency:        transfer.Currency,
			},
			{
				LedgerAccountID: settlementCash.ID,
				Direction:       "credit",
				AmountCents:     transfer.AmountCents,
				Currency:        transfer.Currency,
			},
		},
	})
	return err
}

func (r *Repository) postRefundLedger(ctx context.Context, tx pgx.Tx, transfer dueTransfer, providerReference, reason string) error {
	clearing, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		ReferenceType: "external_clearing",
		ReferenceID:   "sepa_outbound",
		Currency:      transfer.Currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return err
	}
	customerAccount, err := r.ledger.EnsureAccount(ctx, tx, ledger.AccountParams{
		OwnerUserID:   transfer.UserID,
		ReferenceType: "account",
		ReferenceID:   transfer.FromAccountID,
		Currency:      transfer.Currency,
		NormalBalance: "credit",
	})
	if err != nil {
		return err
	}

	_, err = r.ledger.Post(ctx, tx, ledger.PostParams{
		EventType:      "sepa.settlement.failed_refund",
		SourceType:     "sepa_settlement_refund",
		SourceID:       transfer.ID,
		IdempotencyKey: "sepa-settlement-refund-" + transfer.ID,
		Description:    "SEPA outbound settlement failed and funds were refunded",
		Metadata: map[string]any{
			"transfer_id":        transfer.ID,
			"provider":           localSEPAProvider,
			"provider_reference": providerReference,
			"reason":             reason,
		},
		Lines: []ledger.LineParams{
			{
				LedgerAccountID: clearing.ID,
				Direction:       "debit",
				AmountCents:     transfer.AmountCents,
				Currency:        transfer.Currency,
			},
			{
				LedgerAccountID: customerAccount.ID,
				Direction:       "credit",
				AmountCents:     transfer.AmountCents,
				Currency:        transfer.Currency,
			},
		},
	})
	return err
}

func insertEvent(ctx context.Context, tx pgx.Tx, transferID, eventType, statusBefore, statusAfter, providerReference, reason string) error {
	_, err := tx.Exec(ctx, `
		INSERT INTO sepa_settlement_events (
			transfer_id, event_type, status_before, status_after, provider, provider_reference, reason
		)
		VALUES ($1, $2, NULLIF($3, ''), NULLIF($4, ''), $5, NULLIF($6, ''), NULLIF($7, ''))
	`, transferID, eventType, statusBefore, statusAfter, localSEPAProvider, providerReference, reason)
	return err
}

func settlementReference(transferID string, attempt int) string {
	compactID := strings.ToUpper(strings.ReplaceAll(transferID, "-", ""))
	if len(compactID) > 16 {
		compactID = compactID[:16]
	}
	return fmt.Sprintf("LOCAL-SEPA-%s-%02d", compactID, attempt)
}

func localProviderDecision(transfer dueTransfer, attempt int) providerDecision {
	fingerprint := strings.ToUpper(strings.Join([]string{
		transfer.BeneficiaryIBAN,
		transfer.PaymentReference,
		transfer.Description,
	}, " "))

	switch {
	case strings.Contains(fingerprint, "SEPA_FAIL"):
		return providerDecision{
			Outcome: providerPermanentFailure,
			Code:    payments.ProviderCodePermanentFailure,
			Reason:  "local SEPA simulator returned a permanent failure",
		}
	case strings.Contains(fingerprint, "SEPA_RETRY_FAIL"):
		return providerDecision{
			Outcome: providerTemporaryFailure,
			Code:    payments.ProviderCodeTemporaryFailure,
			Reason:  "local SEPA simulator returned a temporary failure",
		}
	case strings.Contains(fingerprint, "SEPA_RETRY") && attempt == 1:
		return providerDecision{
			Outcome: providerTemporaryFailure,
			Code:    payments.ProviderCodeTemporaryFailure,
			Reason:  "local SEPA simulator returned a temporary failure",
		}
	default:
		return providerDecision{
			Outcome: providerSucceeded,
			Code:    payments.ProviderCodeAccepted,
			Reason:  "local SEPA simulator accepted the settlement",
		}
	}
}
