package storage

import (
	"context"
	"errors"
	"fmt"
	"time"

	"git.toki-labs.com/toki/gito/services/core/internal/core"
	"git.toki-labs.com/toki/gito/services/core/internal/events"
	"git.toki-labs.com/toki/gito/services/core/internal/provider"
)

type Store interface {
	Ping(ctx context.Context) error
	Repos() RepoStore
	WorkspaceLeases() WorkspaceLeaseStore
	Operations() OperationStore
	OperationEvents() OperationEventStore
	BranchWatches() BranchWatchStore
	RevisionCursors() RevisionCursorStore
	ProviderDeliveries() ProviderDeliveryStore
	AgentRunInputs() AgentRunInputStore
	WebhookDeliveries() WebhookDeliveryStore
	ProviderConfigs() ProviderConfigStore
}

type RepoStore interface {
	CreateRepo(ctx context.Context, repo core.Repo) error
	GetRepo(ctx context.Context, id string) (core.Repo, error)
	ListRepos(ctx context.Context) ([]core.Repo, error)
	UpdateRepo(ctx context.Context, repo core.Repo) error
}

var (
	ErrInvalidRepo            = errors.New("invalid repo")
	ErrRepoAlreadyExists      = errors.New("repo already exists")
	ErrRepoNotFound           = errors.New("repo not found")
	ErrInvalidLease           = errors.New("invalid workspace lease")
	ErrLeaseAlreadyExists     = errors.New("workspace lease already exists")
	ErrLeaseNotFound          = errors.New("workspace lease not found")
	ErrInvalidOperation       = errors.New("invalid operation")
	ErrOperationAlreadyExists = errors.New("operation already exists")
	ErrOperationNotFound      = errors.New("operation not found")
	ErrAgentRunInputNotFound  = errors.New("agent run input not found")
	ErrInvalidAgentRunInput   = errors.New("invalid agent run input")
	ErrInvalidProviderConfig  = errors.New("invalid provider config")
)

type WorkspaceLeaseStore interface {
	SaveLease(ctx context.Context, lease core.WorkspaceLease) error
	GetLease(ctx context.Context, id string) (core.WorkspaceLease, error)
	ListLeases(ctx context.Context, repoID string) ([]core.WorkspaceLease, error)
	AcquireAvailableLease(ctx context.Context, repoID string, now time.Time, ttl time.Duration) (core.WorkspaceLease, bool, error)
	ReleaseLease(ctx context.Context, id string, release LeaseRelease) (core.WorkspaceLease, error)
}

type LeaseRelease struct {
	OperationState core.OperationState
	Dirty          bool
}

// OperationCreateResult is returned by CreateOperation.
// Created is true when this call inserted a new operation; it is false when an
// existing operation was returned because the idempotency key already existed.
type OperationCreateResult struct {
	Operation core.Operation
	Created   bool
}

type OperationStore interface {
	CreateOperation(ctx context.Context, op core.Operation) (OperationCreateResult, error)
	GetOperation(ctx context.Context, id string) (core.Operation, error)
	ListOperations(ctx context.Context, repoID string) ([]core.Operation, error)
	StartOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
	SucceedOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
	FailOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
	CancelOperation(ctx context.Context, id string, now time.Time) (core.Operation, error)
	PickQueuedOperation(ctx context.Context, now time.Time) (core.Operation, bool, error)
	// PickQueuedAgentRunOperation picks only agent_run-typed operations from
	// the queue. Non-agent operations are never transitioned to running by this
	// method, preventing them from being stranded in a running state.
	PickQueuedAgentRunOperation(ctx context.Context, now time.Time) (core.Operation, bool, error)
}

// AgentRunInputStore persists durable agent run inputs without secret values.
type AgentRunInputStore interface {
	CreateAgentRunInput(ctx context.Context, input core.DurableAgentRunInput) error
	GetAgentRunInput(ctx context.Context, operationID string) (core.DurableAgentRunInput, error)
}

type OperationEvent struct {
	OperationID string
	Event       events.Event
	PublishedAt *time.Time
}

type OperationEventStore interface {
	AppendEvent(ctx context.Context, ev OperationEvent) error
	ListEvents(ctx context.Context, operationID string) ([]OperationEvent, error)
	ListPendingEvents(ctx context.Context, limit int) ([]OperationEvent, error)
	MarkPublished(ctx context.Context, eventID string, now time.Time) (OperationEvent, error)
}

type BranchWatchStore interface {
	UpsertBranchWatch(ctx context.Context, watch core.BranchWatch) (core.BranchWatch, error)
	FindBranchWatch(ctx context.Context, provider, repoID, branch string) (core.BranchWatch, bool, error)
	ListBranchWatches(ctx context.Context) ([]core.BranchWatch, error)
}

type RevisionCursorStore interface {
	UpsertRevisionCursor(ctx context.Context, cursor core.RevisionCursor) error
	GetRevisionCursor(ctx context.Context, repoID, branch string) (core.RevisionCursor, bool, error)
}

// DeliveryResult is returned by RecordOnce.
// First is true when this is the first delivery for the dedupe key.
// When First is false (duplicate), ExistingEventID and ExistingCreatedAt
// hold the values from the original delivery so callers can return a
// consistent idempotent response.
type DeliveryResult struct {
	First             bool
	ExistingEventID   string
	ExistingCreatedAt time.Time
}

// ProviderDeliveryStore records provider deliveries and detects duplicates.
type ProviderDeliveryStore interface {
	RecordOnce(ctx context.Context, delivery core.ProviderDelivery) (DeliveryResult, error)
	DeleteDelivery(ctx context.Context, provider, dedupeKey string) error
}

type WebhookDeliveryCreateResult struct {
	Delivery core.WebhookDelivery
	Created  bool
}

type WebhookDeliveryStore interface {
	EnqueueDelivery(ctx context.Context, delivery core.WebhookDelivery) (WebhookDeliveryCreateResult, error)
	GetDelivery(ctx context.Context, id string) (core.WebhookDelivery, error)
	PickPendingDeliveries(ctx context.Context, limit int, now time.Time) ([]core.WebhookDelivery, error)
	StartDelivery(ctx context.Context, id string, now time.Time) (core.WebhookDelivery, bool, error)
	RecordOutcome(ctx context.Context, id string, status core.WebhookDeliveryStatus, statusCode int, lastErr string, nextAttemptAt time.Time, now time.Time) (core.WebhookDelivery, error)
}

// ProviderConfigStore persists provider configuration.
type ProviderConfigStore interface {
	SaveProviderConfig(ctx context.Context, config provider.Config) (provider.Config, error)
	GetProviderConfig(ctx context.Context, id provider.ProviderID) (provider.Config, bool, error)
	ListProviderConfigs(ctx context.Context) ([]provider.Config, error)
}

// Boundary is the concrete implementation of Store backed by injected sub-stores.
type Boundary struct {
	repos             RepoStore
	leases            WorkspaceLeaseStore
	ops               OperationStore
	opEvents          OperationEventStore
	watches           BranchWatchStore
	cursors           RevisionCursorStore
	deliveries        ProviderDeliveryStore
	agentRunInputs    AgentRunInputStore
	webhookDeliveries WebhookDeliveryStore
	providerConfigs   ProviderConfigStore
}

func New(repos RepoStore, leases WorkspaceLeaseStore, ops OperationStore, opEvents OperationEventStore) *Boundary {
	return &Boundary{
		repos:    repos,
		leases:   leases,
		ops:      ops,
		opEvents: opEvents,
	}
}

// NewWithBranchEvents constructs a Boundary with all sub-stores including branch event stores.
func NewWithBranchEvents(
	repos RepoStore,
	leases WorkspaceLeaseStore,
	ops OperationStore,
	opEvents OperationEventStore,
	watches BranchWatchStore,
	cursors RevisionCursorStore,
	deliveries ProviderDeliveryStore,
	webhookDeliveries WebhookDeliveryStore,
) *Boundary {
	return &Boundary{
		repos:             repos,
		leases:            leases,
		ops:               ops,
		opEvents:          opEvents,
		watches:           watches,
		cursors:           cursors,
		deliveries:        deliveries,
		webhookDeliveries: webhookDeliveries,
	}
}

// NewWithProviderConfigs constructs a Boundary with provider config store support.
func NewWithProviderConfigs(
	repos RepoStore,
	leases WorkspaceLeaseStore,
	ops OperationStore,
	opEvents OperationEventStore,
	providerConfigs ProviderConfigStore,
) *Boundary {
	return &Boundary{
		repos:           repos,
		leases:          leases,
		ops:             ops,
		opEvents:        opEvents,
		providerConfigs: providerConfigs,
	}
}

func (b *Boundary) Ping(ctx context.Context) error {
	if ctx == nil {
		return fmt.Errorf("storage: nil context")
	}
	if b.repos == nil || b.leases == nil || b.ops == nil || b.opEvents == nil {
		return fmt.Errorf("storage: one or more sub-stores not injected")
	}
	return nil
}

func (b *Boundary) Repos() RepoStore                          { return b.repos }
func (b *Boundary) WorkspaceLeases() WorkspaceLeaseStore      { return b.leases }
func (b *Boundary) Operations() OperationStore                { return b.ops }
func (b *Boundary) OperationEvents() OperationEventStore      { return b.opEvents }
func (b *Boundary) BranchWatches() BranchWatchStore           { return b.watches }
func (b *Boundary) RevisionCursors() RevisionCursorStore      { return b.cursors }
func (b *Boundary) ProviderDeliveries() ProviderDeliveryStore { return b.deliveries }
func (b *Boundary) AgentRunInputs() AgentRunInputStore        { return b.agentRunInputs }
func (b *Boundary) WebhookDeliveries() WebhookDeliveryStore {
	if b.webhookDeliveries == nil {
		return nil
	}
	return b.webhookDeliveries
}

func (b *Boundary) ProviderConfigs() ProviderConfigStore {
	return b.providerConfigs
}
