// Package binding owns persistence for Workflow Binding aggregates,
// immutable Binding revisions, and mutable activation sources.
package binding

import (
	"bytes"
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"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
	maxListLimit     = 101
)

var (
	ErrNotFound     = storeerr.ErrNotFound
	ErrConflict     = storeerr.ErrConflict
	ErrBadRequest   = storeerr.ErrBadRequest
	ErrArchived     = errors.New("workflow binding is archived")
	ErrPlanArchived = errors.New("workflow Plan is archived")
	ErrInvalidState = errors.New("workflow binding state does not allow this operation")
)

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 binding " + 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} }

func (store *Store) Validate() error {
	if store == nil || store.db == nil {
		return fmt.Errorf("workflow binding 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() {
		value = time.Now()
	}
	// Binding timestamps and schedule cursors use DATETIME(6). Return the exact
	// durable representation so an immediate mutation result and its later
	// idempotent replay cannot differ only by sub-microsecond precision.
	return value.UTC().Truncate(time.Microsecond)
}

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 validateOwner(owner Owner) (Owner, error) {
	owner.UserID = trim(owner.UserID)
	owner.MemberID = trim(owner.MemberID)
	if owner.UserID == "" || owner.MemberID == "" {
		return Owner{}, storeerr.BadRequest("owner userId and memberId are required")
	}
	return owner, nil
}

func validateSourceKind(value string) (string, error) {
	value = trim(value)
	if value != SourceKindSchedule {
		return "", storeerr.BadRequest("source kind must be schedule")
	}
	return value, nil
}

func normalizeConfigJSON(value []byte) ([]byte, error) {
	if len(value) == 0 || !json.Valid(value) {
		return nil, storeerr.BadRequest("canonical binding config must be valid JSON")
	}
	var object map[string]json.RawMessage
	if err := json.Unmarshal(value, &object); err != nil || object == nil {
		return nil, storeerr.BadRequest("canonical binding config must be a JSON object")
	}
	var compacted bytes.Buffer
	compacted.Grow(len(value))
	if err := json.Compact(&compacted, value); err != nil {
		return nil, storeerr.BadRequest("canonical binding config must be valid JSON")
	}
	return compacted.Bytes(), nil
}

func requireConfigSourceKind(configJSON []byte, sourceKind string) error {
	var envelope struct {
		Source struct {
			Type string `json:"type"`
		} `json:"source"`
	}
	if err := json.Unmarshal(configJSON, &envelope); err != nil {
		return storeerr.BadRequest("canonical binding config must be valid JSON")
	}
	if trim(envelope.Source.Type) != sourceKind {
		return storeerr.BadRequest("canonical binding config source type does not match source kind")
	}
	return nil
}

func validateDigest(value string) (string, error) {
	value = trim(value)
	if !strings.HasPrefix(value, "sha256:") {
		return "", storeerr.BadRequest("binding revision digest must use the sha256:<hex> format")
	}
	decoded, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
	if err != nil || len(decoded) != sha256.Size {
		return "", storeerr.BadRequest("binding revision digest must use the sha256:<hex> format")
	}
	return value, nil
}

func newID(prefix string) (string, error) {
	id, err := identifiers.NewEntityID(prefix)
	if err != nil {
		return "", fmt.Errorf("generate workflow binding id: %w", 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 normalizeCommandRequestDigest(value string, fallback string) (string, error) {
	value = trim(value)
	if value == "" {
		value = trim(fallback)
	}
	decoded, err := hex.DecodeString(value)
	if err != nil || len(decoded) != sha256.Size || value != strings.ToLower(value) {
		return "", storeerr.BadRequest("workflow binding request digest must be 64 lowercase hexadecimal characters")
	}
	return value, nil
}

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 binding not found")
	}
	if coredb.IsUniqueViolation(err) {
		return storeerr.Conflict("workflow binding already exists")
	}
	if coredb.IsForeignKeyViolation(err) {
		return storeerr.BadRequest("workflow binding references a missing or mismatched Plan version")
	}
	return err
}

// requireActivePlanVersion is the commit-time lifecycle fence shared by
// Binding creation and revision append. The manage layer validates artifacts
// before entering persistence, but only this row lock can serialize the final
// write against Plan archive. Idempotency receipts must be checked before this
// helper so an already-committed command remains replayable after later archive.
func requireActivePlanVersion(ctx context.Context, queryer coredb.QueryRower, workspaceID string, planID string, planVersionID string) error {
	var archivedAt sql.NullTime
	err := queryer.QueryRow(ctx, `
SELECT p.archived_at
FROM workflow_plans p
WHERE p.workspace_id = $1 AND p.id = $2
FOR UPDATE
`, workspaceID, planID).Scan(&archivedAt)
	if coredb.IsNotFound(err) {
		return storeerr.BadRequest("workflow binding Plan version does not exist for the selected Plan")
	}
	if err != nil {
		return fmt.Errorf("lock workflow binding Plan: %w", err)
	}

	var lockedVersionID string
	err = queryer.QueryRow(ctx, `
SELECT id
FROM workflow_plan_versions
WHERE workspace_id = $1 AND plan_id = $2 AND id = $3
FOR UPDATE
`, workspaceID, planID, planVersionID).Scan(&lockedVersionID)
	if coredb.IsNotFound(err) {
		return storeerr.BadRequest("workflow binding Plan version does not exist for the selected Plan")
	}
	if err != nil {
		return fmt.Errorf("lock workflow binding Plan version: %w", err)
	}
	if archivedAt.Valid {
		return ErrPlanArchived
	}
	return nil
}
