package persistence

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

const (
	runtimeApprovalInvocationTable     = "_System_RuntimeApprovalInvocation"
	RuntimeApprovalInvocationAccepted  = "ACCEPTED"
	RuntimeApprovalInvocationCompleted = "COMPLETED"
)

var (
	ErrRuntimeApprovalInvocationConflict              = errors.New("runtime Approval invocation idempotency identity is bound to different content")
	ErrRuntimeApprovalInvocationServingBindingChanged = errors.New("runtime Approval invocation is bound to a different serving revision")
	ErrRuntimeApprovalInvocationTransactionRequired   = errors.New("runtime Approval invocation requires an active caller-owned transaction")
)

var runtimeApprovalInvocationRequiredColumns = map[string]string{
	"Id": "varchar(255)", "ScopeDigest": "char(64)",
	"IdempotencyKey": "varchar(255)", "Operation": "varchar(16)",
	"DefinitionId": "varchar(255)", "DefinitionVersion": "varchar(255)",
	"ClosureDigest": "char(71)", "RequestDigest": "char(71)",
	"Receipts": "json", "State": "varchar(16)",
	"ResultPayload": "mediumblob", "CreatedDate": "datetime(6)",
	"LastModifiedDate": "datetime(6)",
}

type RuntimeApprovalInvocationRepository struct {
	db           *sql.DB
	transactions *TransactionManager
}

func NewRuntimeApprovalInvocationRepository(db *sql.DB, transactions *TransactionManager) *RuntimeApprovalInvocationRepository {
	return &RuntimeApprovalInvocationRepository{db: db, transactions: transactions}
}

func VerifyRuntimeApprovalInvocationSchema(ctx context.Context, db *sql.DB) error {
	if ctx == nil || db == nil {
		return errors.New("runtime Approval invocation schema: context and database are required")
	}
	if err := VerifyRequiredTableColumns(ctx, db, runtimeApprovalInvocationTable, runtimeApprovalInvocationRequiredColumns); err != nil {
		return fmt.Errorf("runtime Approval invocation schema: %w", err)
	}
	if err := VerifyExactTableIndex(ctx, db, runtimeApprovalInvocationTable, "PRIMARY", true, []string{"Id"}); err != nil {
		return fmt.Errorf("runtime Approval invocation schema: %w", err)
	}
	if err := VerifyExactTableIndex(ctx, db, runtimeApprovalInvocationTable, "ux_runtime_approval_invocation_scope", true, []string{"ScopeDigest", "IdempotencyKey"}); err != nil {
		return fmt.Errorf("runtime Approval invocation schema: %w", err)
	}
	return nil
}

type RuntimeApprovalInvocationAcceptance struct {
	RowID, ScopeDigest, IdempotencyKey, Operation                 string
	DefinitionID, DefinitionVersion, ClosureDigest, RequestDigest string
	Receipts                                                      []byte
}

type RuntimeApprovalInvocationRow struct {
	RowID, ScopeDigest, IdempotencyKey, Operation                 string
	DefinitionID, DefinitionVersion, ClosureDigest, RequestDigest string
	Receipts                                                      []byte
	State                                                         string
	ResultPayload                                                 []byte
}

func (r *RuntimeApprovalInvocationRepository) Lookup(ctx context.Context, scopeDigest, idempotencyKey string) (RuntimeApprovalInvocationRow, bool, error) {
	if r == nil || r.db == nil || r.transactions == nil || !hexDigestPattern.MatchString(scopeDigest) || idempotencyKey == "" {
		return RuntimeApprovalInvocationRow{}, false, errors.New("runtime Approval invocation lookup is incomplete")
	}
	exec, err := r.activeTransaction(ctx)
	if err != nil {
		return RuntimeApprovalInvocationRow{}, false, err
	}
	row, found, err := r.loadOne(ctx, exec, scopeDigest, idempotencyKey)
	if err != nil {
		return RuntimeApprovalInvocationRow{}, false, err
	}
	return row, found, nil
}

func (r *RuntimeApprovalInvocationRepository) Accept(ctx context.Context, input RuntimeApprovalInvocationAcceptance) (RuntimeApprovalInvocationRow, bool, error) {
	if r == nil || r.db == nil || r.transactions == nil || input.RowID == "" ||
		!hexDigestPattern.MatchString(input.ScopeDigest) || input.IdempotencyKey == "" ||
		input.Operation == "" || input.DefinitionID == "" || input.DefinitionVersion == "" ||
		!validPrefixedSHA256(input.ClosureDigest) || !validPrefixedSHA256(input.RequestDigest) || len(input.Receipts) == 0 {
		return RuntimeApprovalInvocationRow{}, false, errors.New("runtime Approval invocation acceptance is incomplete")
	}
	exec, err := r.activeTransaction(ctx)
	if err != nil {
		return RuntimeApprovalInvocationRow{}, false, err
	}
	result, err := exec.ExecContext(ctx, `INSERT IGNORE INTO _System_RuntimeApprovalInvocation
(Id, ScopeDigest, IdempotencyKey, Operation, DefinitionId, DefinitionVersion, ClosureDigest, RequestDigest, Receipts, State)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'ACCEPTED')`,
		input.RowID, input.ScopeDigest, input.IdempotencyKey, input.Operation,
		input.DefinitionID, input.DefinitionVersion, input.ClosureDigest,
		input.RequestDigest, input.Receipts,
	)
	if err != nil {
		return RuntimeApprovalInvocationRow{}, false, fmt.Errorf("accept runtime Approval invocation: %w", err)
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return RuntimeApprovalInvocationRow{}, false, fmt.Errorf("accept runtime Approval invocation rows affected: %w", err)
	}
	row, found, err := r.loadOne(ctx, exec, input.ScopeDigest, input.IdempotencyKey)
	if err != nil {
		return RuntimeApprovalInvocationRow{}, false, err
	}
	if !found || row.RowID != input.RowID || row.ScopeDigest != input.ScopeDigest || row.IdempotencyKey != input.IdempotencyKey {
		return row, true, ErrRuntimeApprovalInvocationConflict
	}
	sameReceipts, err := equivalentJSONDocuments(row.Receipts, input.Receipts)
	if err != nil {
		return row, true, fmt.Errorf("compare runtime Approval invocation receipts: %w", err)
	}
	if !sameReceipts {
		return row, true, ErrRuntimeApprovalInvocationServingBindingChanged
	}
	if row.Operation != input.Operation || row.DefinitionID != input.DefinitionID ||
		row.DefinitionVersion != input.DefinitionVersion || row.ClosureDigest != input.ClosureDigest ||
		row.RequestDigest != input.RequestDigest {
		return row, true, ErrRuntimeApprovalInvocationConflict
	}
	return row, affected == 0, nil
}

func (r *RuntimeApprovalInvocationRepository) Complete(ctx context.Context, rowID string, resultPayload []byte) error {
	if r == nil || r.db == nil || r.transactions == nil || rowID == "" || len(resultPayload) == 0 {
		return errors.New("runtime Approval invocation completion is incomplete")
	}
	exec, err := r.activeTransaction(ctx)
	if err != nil {
		return err
	}
	result, err := exec.ExecContext(ctx, `UPDATE _System_RuntimeApprovalInvocation
SET State = 'COMPLETED', ResultPayload = ?, LastModifiedDate = CURRENT_TIMESTAMP(6)
WHERE Id = ? AND State = 'ACCEPTED'`, resultPayload, rowID)
	if err != nil {
		return fmt.Errorf("complete runtime Approval invocation: %w", err)
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("complete runtime Approval invocation rows affected: %w", err)
	}
	if affected != 1 {
		return errors.New("runtime Approval invocation completion lost ownership")
	}
	return nil
}

func (r *RuntimeApprovalInvocationRepository) loadOne(ctx context.Context, exec Executor, scopeDigest, idempotencyKey string) (RuntimeApprovalInvocationRow, bool, error) {
	row := exec.QueryRowContext(ctx, `SELECT Id, ScopeDigest, IdempotencyKey, Operation, DefinitionId,
       DefinitionVersion, ClosureDigest, RequestDigest, Receipts, State, COALESCE(ResultPayload, '')
  FROM _System_RuntimeApprovalInvocation
 WHERE ScopeDigest = ? AND IdempotencyKey = ?
 LIMIT 1`, scopeDigest, idempotencyKey)
	var result RuntimeApprovalInvocationRow
	if err := row.Scan(&result.RowID, &result.ScopeDigest, &result.IdempotencyKey, &result.Operation,
		&result.DefinitionID, &result.DefinitionVersion, &result.ClosureDigest,
		&result.RequestDigest, &result.Receipts, &result.State, &result.ResultPayload); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return RuntimeApprovalInvocationRow{}, false, nil
		}
		return RuntimeApprovalInvocationRow{}, false, fmt.Errorf("load runtime Approval invocation: %w", err)
	}
	return result, true, nil
}

func (r *RuntimeApprovalInvocationRepository) activeTransaction(ctx context.Context) (*sql.Tx, error) {
	if r == nil || r.transactions == nil {
		return nil, ErrRuntimeApprovalInvocationTransactionRequired
	}
	tx, ok := r.transactions.ExtractTx(ctx).(*sql.Tx)
	if !ok || tx == nil {
		return nil, ErrRuntimeApprovalInvocationTransactionRequired
	}
	return tx, nil
}
