// Copyright (c) certctl
// SPDX-License-Identifier: BSL-1.1

// WithinTx is the transactional spine for any service-layer operation
// whose audit row must be atomic with the underlying state change.
// Closes the #3 acquisition-readiness blocker from the 2026-05-01
// issuer coverage audit (Part 1.5 finding #1: audit row not
// transactional with issuance).
//
// The Querier interface lives in internal/repository (shared with the
// interface declarations) so repository interfaces and the postgres
// concrete types reference the same type without a circular import.
package postgres

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

	"github.com/certctl-io/certctl/internal/repository"
)

// transactor is the production implementation of repository.Transactor.
// It wraps a *sql.DB and exposes the WithinTx helper as the interface
// method service-layer code calls.
type transactor struct {
	db *sql.DB
}

// NewTransactor returns a repository.Transactor backed by the given
// *sql.DB. Production wiring (cmd/server/main.go) passes the same db
// handle that backs the other repositories; tests pass a mock that
// implements the interface against in-memory state.
func NewTransactor(db *sql.DB) repository.Transactor {
	return &transactor{db: db}
}

// WithinTx delegates to the package-level WithinTx helper, adapting
// the function signature so callers receive repository.Querier instead
// of *sql.Tx (which the interface requires for portability across
// transactor implementations).
func (t *transactor) WithinTx(ctx context.Context, fn func(q repository.Querier) error) error {
	return WithinTx(ctx, t.db, func(tx *sql.Tx) error {
		return fn(tx)
	})
}

// Querier is re-exported from the parent repository package so callers
// inside this package can reference it without an extra import.
//
// Deprecated: external callers should use repository.Querier directly.
// This alias exists for legibility within the postgres package only.

// WithinTx runs fn inside a transaction. The transaction is committed
// if fn returns nil; rolled back if fn returns an error or panics.
//
// Contract:
//
//   - On nil error from fn: tx.Commit() is called. If Commit fails
//     (e.g., serialization conflict, connection drop), the commit
//     error is returned.
//   - On non-nil error from fn: tx.Rollback() is called. If Rollback
//     itself errors, the original fn error is wrapped with the
//     rollback error so operators see both.
//   - On panic in fn: tx.Rollback() is called and the panic is
//     re-raised. The transaction is never left dangling.
//
// Callers must NOT call tx.Commit() or tx.Rollback() inside fn — that's
// WithinTx's job. Returning an error from fn signals "roll back";
// returning nil signals "commit".
//
// BeginTx is called with nil opts; callers needing isolation level
// other than the database default should construct their own tx via
// db.BeginTx and not use this helper.
func WithinTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) (err error) {
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin tx: %w", err)
	}

	defer func() {
		if p := recover(); p != nil {
			_ = tx.Rollback()
			panic(p)
		}
		if err != nil {
			if rbErr := tx.Rollback(); rbErr != nil {
				err = fmt.Errorf("%w; rollback: %v", err, rbErr)
			}
		}
	}()

	if err = fn(tx); err != nil {
		return err
	}
	if cmErr := tx.Commit(); cmErr != nil {
		return fmt.Errorf("commit tx: %w", cmErr)
	}
	return nil
}
