package app

import (
	"context"
	"errors"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
	"github.com/tidbcloud/AgentTeam/internal/store/channel"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

var (
	ErrUnauthorized             = storeerr.ErrUnauthorized
	ErrForbidden                = storeerr.ErrForbidden
	ErrNotFound                 = storeerr.ErrNotFound
	ErrConflict                 = storeerr.ErrConflict
	ErrBadRequest               = storeerr.ErrBadRequest
	ErrGone                     = storeerr.ErrGone
	ErrSystemNotInitialized     = storeerr.ErrSystemNotInitialized
	ErrSystemAlreadyInitialized = storeerr.ErrSystemAlreadyInitialized
	// ErrAppInstallationApprovalStale classifies a pending proposal whose
	// immutable base no longer matches current workspace state. It deliberately
	// does not carry an HTTP status by itself; callers keep the original typed
	// error while Approval uses this sentinel to stop retrying a decision
	// that can only be rejected and resubmitted.
	ErrAppInstallationApprovalStale = errors.New("App installation approval is stale")
)

type appInstallationApprovalStaleError struct {
	cause error
}

func (err appInstallationApprovalStaleError) Error() string {
	return err.cause.Error()
}

func (err appInstallationApprovalStaleError) Unwrap() []error {
	return []error{err.cause, ErrAppInstallationApprovalStale}
}

func markAppInstallationApprovalStale(err error) error {
	if err == nil || errors.Is(err, ErrAppInstallationApprovalStale) {
		return err
	}
	return appInstallationApprovalStaleError{cause: err}
}

func markAppInstallationApprovalStaleIfDeterministic(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, ErrConflict) || errors.Is(err, ErrBadRequest) ||
		errors.Is(err, ErrForbidden) || errors.Is(err, ErrNotFound) || errors.Is(err, ErrGone) {
		return markAppInstallationApprovalStale(err)
	}
	return err
}

func badRequest(message string) error  { return storeerr.BadRequest(message) }
func forbidden(message string) error   { return storeerr.Forbidden(message) }
func notFound(message string) error    { return storeerr.NotFound(message) }
func conflict(message string) error    { return storeerr.Conflict(message) }
func gone(message string) error        { return storeerr.Gone(message) }
func isUniqueViolation(err error) bool { return coredb.IsUniqueViolation(err) }
func uniqueViolationConstraint(err error) (string, bool) {
	return coredb.UniqueViolationConstraint(err)
}

func rollback(tx coredb.Tx, ctx context.Context) {
	if tx != nil {
		_ = tx.Rollback(ctx)
	}
}

func withTxValue[T any](ctx context.Context, db coredb.TxBeginner, fn func(coredb.Tx) (T, error)) (T, error) {
	var value T
	err := coredb.WithTx(ctx, db, func(tx coredb.Tx) error {
		var err error
		value, err = fn(tx)
		return err
	})
	if err != nil {
		var zero T
		return zero, err
	}
	return value, nil
}

func insertChannelMembershipIfMissing(ctx context.Context, execer channel.CommandTagExecer, channelID string, memberID string, createdAt time.Time) error {
	return channel.InsertChannelMembershipIfMissing(ctx, execer, channelID, memberID, createdAt)
}

func uniqueViolationMatches(err error, constraints ...string) bool {
	return channel.UniqueViolationMatches(err, constraints...)
}

func execWithinSavepoint(ctx context.Context, tx coredb.Tx, savepointName string, query string, args ...any) error {
	return channel.ExecWithinSavepoint(ctx, tx, savepointName, query, args...)
}

func (store *Store) channelByID(ctx context.Context, queryer interface {
	QueryRow(context.Context, string, ...any) coredb.Row
}, workspaceID string, channelID string) (access.Channel, error) {
	channel, err := access.LookupChannel(ctx, queryer, workspaceID, channelID)
	if errors.Is(err, access.ErrNotFound) {
		return access.Channel{}, ErrNotFound
	}
	if err != nil {
		return access.Channel{}, err
	}
	return channel, nil
}

func (store *Store) ensureMemberInWorkspace(ctx context.Context, queryer interface {
	QueryRow(context.Context, string, ...any) coredb.Row
}, workspaceID string, memberID string) error {
	err := access.EnsureMemberInWorkspace(ctx, queryer, workspaceID, memberID)
	if errors.Is(err, access.ErrNotFound) {
		return ErrNotFound
	}
	return err
}
