// Package plan owns persistence for the Workflow Plan authoring
// aggregate. It is intentionally separate from internal/store/plan, which
// owns subscription and seat-billing plan data.
package plan

import (
	"crypto/sha256"
	"database/sql"
	"encoding/binary"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	defaultListLimit = 50
	// The API owner caps user-visible pages at 100 and requests one sentinel
	// row to determine whether an opaque next cursor exists.
	maxListLimit = 101
)

var (
	ErrNotFound   = storeerr.ErrNotFound
	ErrConflict   = storeerr.ErrConflict
	ErrBadRequest = storeerr.ErrBadRequest
	ErrArchived   = errors.New("workflow plan is archived")
	// ErrAgentEditWakeActive prevents the responding Agent from crossing the
	// Draft-only Builder-edit boundary with a Plan lifecycle mutation.
	ErrAgentEditWakeActive = errors.New("workflow Agent edit wake is active")
	// ErrInvalidVersionArtifact fails Binding deployment closed when immutable
	// artifact bytes disagree with their envelope or indexed metadata.
	ErrInvalidVersionArtifact = errors.New("workflow plan version artifact is invalid")
)

type RevisionConflictError struct {
	Resource string
	Expected int64
	Actual   int64
}

func (err RevisionConflictError) Error() string {
	return fmt.Sprintf("%s revision conflict: expected %d, actual %d", err.Resource, err.Expected, err.Actual)
}

func (RevisionConflictError) Unwrap() error { return ErrConflict }

type IdempotencyConflictError struct {
	Operation string
}

func (err IdempotencyConflictError) Error() string {
	return "workflow plan " + err.Operation + " idempotency key was already used for a different request"
}

func (IdempotencyConflictError) Unwrap() error { return ErrConflict }

type Store struct {
	db coredb.DB
}

func New(db coredb.DB) *Store { return &Store{db: db} }

// TxStore binds Workflow Plan mutations to a caller-owned transaction. Domain
// services use this surface when Plan state must commit atomically with another
// aggregate, such as a Team Workflow Approval decision.
type TxStore struct {
	tx coredb.Tx
}

// InTx returns a Workflow Plan store backed by the caller-owned transaction.
// The caller remains responsible for committing or rolling back that
// transaction.
func (store *Store) InTx(tx coredb.Tx) *TxStore { return &TxStore{tx: tx} }

func (store *TxStore) validate() error {
	if store == nil || store.tx == nil {
		return fmt.Errorf("workflow plan tx store is not configured")
	}
	return nil
}

func (store *Store) Validate() error {
	if store == nil || store.db == nil {
		return fmt.Errorf("workflow plan store database is not configured")
	}
	return nil
}

func trim(value string) string { return strings.TrimSpace(value) }

func utcOrNow(value time.Time) time.Time {
	if value.IsZero() {
		return time.Now().UTC()
	}
	return value.UTC()
}

func normalizeLimit(limit int) int {
	if limit <= 0 {
		return defaultListLimit
	}
	if limit > maxListLimit {
		return maxListLimit
	}
	return limit
}

func validateActor(actor Actor) (Actor, error) {
	actor.Type = trim(actor.Type)
	actor.ID = trim(actor.ID)
	actor.MemberID = trim(actor.MemberID)
	if actor.Type != ActorTypeHuman && actor.Type != ActorTypeAgent {
		return Actor{}, storeerr.BadRequest("actor type must be human or agent")
	}
	if actor.ID == "" || actor.MemberID == "" {
		return Actor{}, storeerr.BadRequest("actor id and memberId are required")
	}
	return actor, nil
}

func newID(prefix string, label string) (string, error) {
	id, err := identifiers.NewEntityID(prefix)
	if err != nil {
		return "", fmt.Errorf("generate workflow plan %s id: %w", label, err)
	}
	return id, nil
}

func requestDigest(parts ...string) string {
	hash := sha256.New()
	var length [8]byte
	for _, part := range parts {
		binary.BigEndian.PutUint64(length[:], uint64(len(part)))
		_, _ = hash.Write(length[:])
		_, _ = hash.Write([]byte(part))
	}
	return fmt.Sprintf("%x", hash.Sum(nil))
}

func boolText(value bool) string { return strconv.FormatBool(value) }

func intText(value int64) string { return strconv.FormatInt(value, 10) }

func ptrString(value sql.NullString) *string {
	if !value.Valid {
		return nil
	}
	copy := value.String
	return &copy
}

func ptrInt64(value sql.NullInt64) *int64 {
	if !value.Valid {
		return nil
	}
	copy := value.Int64
	return &copy
}

func ptrTime(value sql.NullTime) *time.Time {
	if !value.Valid {
		return nil
	}
	copy := value.Time.UTC()
	return &copy
}

func mapWriteError(err error) error {
	if err == nil {
		return nil
	}
	if coredb.IsNotFound(err) {
		return storeerr.NotFound("workflow plan not found")
	}
	if coredb.IsUniqueViolation(err) {
		return storeerr.Conflict("workflow plan already exists")
	}
	return err
}
