package review

import (
	"context"
	"fmt"
	"strings"

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

var (
	threadReadAction = access.HumanAction{
		Resource: access.ResourceThread,
		Action:   access.ActionRead,
	}
)

var (
	ErrUnauthorized = storeerr.ErrUnauthorized
	ErrNotFound     = storeerr.ErrNotFound
	ErrForbidden    = storeerr.ErrForbidden
)

func badRequest(message string) error { return storeerr.BadRequest(message) }

// Queryer is the read-only query surface needed by the review store.
type Queryer interface {
	QueryRow(context.Context, string, ...any) coredb.Row
	Query(context.Context, string, ...any) (coredb.Rows, error)
}

// Options declares cross-domain dependencies injected by the root Store.
type Options struct {
	// LoadAuthorizedHumanThreadAccess authorizes a human user for thread read access.
	LoadAuthorizedHumanThreadAccess func(ctx context.Context, queryer Queryer, userID string, workspaceID string, threadChannelID string, action access.HumanAction) (access.HumanThreadAccess, error)

	// RequireAgentReviewRead authorizes a human user to read agent review data.
	RequireAgentReviewRead func(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error)

	// RequireAgentOperation authorizes a human user to manage an agent.
	RequireAgentOperation func(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error)

	// AgentByID loads an agent view by workspace and agent ID.
	AgentByID func(ctx context.Context, queryer Queryer, workspaceID string, agentID string) (agent.AgentView, error)

	// LoadHumanWorkspaceMember optionally overrides the workspace-member
	// resolver used by project authorization paths (CreateProjectForUser,
	// GetProjectForUser, resolveProjectRole, GetOrCreateProjectForUser, and the
	// C4.3 review→project resolver). When nil, access.LoadHumanWorkspaceMember
	// is used directly. Tests stub this to bypass the real members-table lookup
	// so review-only regression tests don't need to seed workspace members.
	LoadHumanWorkspaceMember func(ctx context.Context, queryer Queryer, userID string, workspaceID string) (access.HumanWorkspaceMember, error)
}

// Store owns review-context and review-feedback persistence.
type Store struct {
	db      coredb.DB
	options Options

	// onUpdatePreCheckPassed, if non-nil, is invoked by
	// UpdateReviewContextForUser AFTER the identity pre-check passes but
	// BEFORE the UPDATE executes. Production leaves this nil. Test-only: lets
	// a regression deterministically force the SQL 1062 path that the
	// IsUniqueViolation branch must map to typed Conflict. The hook runs on
	// the SAME connection as the UPDATE (rollback DB is single-connection), so
	// a row the hook inserts is visible to the UPDATE that follows. Without
	// this hook the SQL-constraint path can only be exercised via probabilistic
	// concurrent racing, which is flaky under TiDB testkit snapshot quirks.
	onUpdatePreCheckPassed func(ctx context.Context, workspaceID, targetHash, skipID string)
}

// New creates a new review Store with the given database connection and options.
func New(db coredb.DB, options Options) *Store {
	return &Store{db: db, options: options}
}

func (s *Store) Validate() error { return s.options.Validate() }

func (o Options) Validate() error {
	var missing []string
	if o.LoadAuthorizedHumanThreadAccess == nil {
		missing = append(missing, "LoadAuthorizedHumanThreadAccess")
	}
	if o.RequireAgentReviewRead == nil {
		missing = append(missing, "RequireAgentReviewRead")
	}
	if o.RequireAgentOperation == nil {
		missing = append(missing, "RequireAgentOperation")
	}
	if o.AgentByID == nil {
		missing = append(missing, "AgentByID")
	}
	if len(missing) > 0 {
		return fmt.Errorf("review store: missing dependencies: %s", strings.Join(missing, ", "))
	}
	return nil
}

// --- Dependency wrappers with nil-checks ---

func (store *Store) loadAuthorizedHumanThreadAccess(ctx context.Context, queryer Queryer, userID string, workspaceID string, threadChannelID string, action access.HumanAction) (access.HumanThreadAccess, error) {
	if store == nil || store.options.LoadAuthorizedHumanThreadAccess == nil {
		return access.HumanThreadAccess{}, fmt.Errorf("review store load authorized human thread access dependency is not configured")
	}
	return store.options.LoadAuthorizedHumanThreadAccess(ctx, queryer, userID, workspaceID, threadChannelID, action)
}

func (store *Store) requireAgentReviewRead(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error) {
	if store == nil || store.options.RequireAgentReviewRead == nil {
		return access.HumanAgentAccess{}, fmt.Errorf("review store require agent review read dependency is not configured")
	}
	return store.options.RequireAgentReviewRead(ctx, userID, workspaceID, agentID)
}

func (store *Store) requireAgentOperation(ctx context.Context, userID string, workspaceID string, agentID string) (access.HumanAgentAccess, error) {
	if store == nil || store.options.RequireAgentOperation == nil {
		return access.HumanAgentAccess{}, fmt.Errorf("review store require agent operation dependency is not configured")
	}
	return store.options.RequireAgentOperation(ctx, userID, workspaceID, agentID)
}

func (store *Store) agentByID(ctx context.Context, queryer Queryer, workspaceID string, agentID string) (agent.AgentView, error) {
	if store == nil || store.options.AgentByID == nil {
		return agent.AgentView{}, fmt.Errorf("review store agent by id dependency is not configured")
	}
	return store.options.AgentByID(ctx, queryer, workspaceID, agentID)
}

// loadHumanWorkspaceMember resolves the workspace member for a human caller.
// It delegates to the Options callback when configured (tests); otherwise it
// falls back to access.LoadHumanWorkspaceMember so production stays on the
// real members-table lookup without needing to wire the option explicitly.
func (store *Store) loadHumanWorkspaceMember(ctx context.Context, queryer Queryer, userID string, workspaceID string) (access.HumanWorkspaceMember, error) {
	if store != nil && store.options.LoadHumanWorkspaceMember != nil {
		return store.options.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
	}
	return access.LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
}

// --- Shared helpers ---

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 execInsertWithOptionalSavepoint(ctx context.Context, execer channel.CommandTagExecer, savepointName string, query string, args ...any) error {
	if tx, ok := execer.(coredb.Tx); ok {
		return execWithinSavepoint(ctx, tx, savepointName, query, args...)
	}
	_, err := execer.Exec(ctx, query, args...)
	return err
}
