package models

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"
	"slices"
	"strings"
	"time"

	"github.com/bluesky-social/indigo/api/atproto"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/bluesky-social/indigo/xrpc"
	"tangled.org/core/api/tangled"
	"tangled.org/core/idresolver"
)

type ConcreteType string

const (
	ConcreteTypeNull   ConcreteType = "null"
	ConcreteTypeString ConcreteType = "string"
	ConcreteTypeInt    ConcreteType = "integer"
	ConcreteTypeBool   ConcreteType = "boolean"
)

type ValueTypeFormat string

const (
	ValueTypeFormatAny ValueTypeFormat = "any"
	ValueTypeFormatDid ValueTypeFormat = "did"
)

// ValueType represents an atproto lexicon type definition with constraints
type ValueType struct {
	Type   ConcreteType    `json:"type"`
	Format ValueTypeFormat `json:"format,omitempty"`
	Enum   []string        `json:"enum,omitempty"`
}

func (vt *ValueType) AsRecord() tangled.LabelDefinition_ValueType {
	return tangled.LabelDefinition_ValueType{
		Type:   string(vt.Type),
		Format: string(vt.Format),
		Enum:   vt.Enum,
	}
}

func ValueTypeFromRecord(record tangled.LabelDefinition_ValueType) ValueType {
	return ValueType{
		Type:   ConcreteType(record.Type),
		Format: ValueTypeFormat(record.Format),
		Enum:   record.Enum,
	}
}

func (vt ValueType) IsConcreteType() bool {
	return vt.Type == ConcreteTypeNull ||
		vt.Type == ConcreteTypeString ||
		vt.Type == ConcreteTypeInt ||
		vt.Type == ConcreteTypeBool
}

func (vt ValueType) IsNull() bool {
	return vt.Type == ConcreteTypeNull
}

func (vt ValueType) IsString() bool {
	return vt.Type == ConcreteTypeString
}

func (vt ValueType) IsInt() bool {
	return vt.Type == ConcreteTypeInt
}

func (vt ValueType) IsBool() bool {
	return vt.Type == ConcreteTypeBool
}

func (vt ValueType) IsEnum() bool {
	return len(vt.Enum) > 0
}

func (vt ValueType) IsDidFormat() bool {
	return vt.Format == ValueTypeFormatDid
}

func (vt ValueType) IsAnyFormat() bool {
	return vt.Format == ValueTypeFormatAny
}

type LabelDefinition struct {
	Id   int64
	Did  string
	Rkey string

	Name      string
	ValueType ValueType
	Scope     []string
	Color     *string
	Multiple  bool
	Created   time.Time
}

func (l *LabelDefinition) AtUri() syntax.ATURI {
	return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", l.Did, tangled.LabelDefinitionNSID, l.Rkey))
}

func (l *LabelDefinition) AsRecord() tangled.LabelDefinition {
	vt := l.ValueType.AsRecord()
	return tangled.LabelDefinition{
		Name:      l.Name,
		Color:     l.Color,
		CreatedAt: l.Created.Format(time.RFC3339),
		Multiple:  &l.Multiple,
		Scope:     l.Scope,
		ValueType: &vt,
	}
}

var (
	// Label name should be alphanumeric with hyphens/underscores, but not start/end with them
	labelNameRegex = regexp.MustCompile(`^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$`)
	// Color should be a valid hex color
	colorRegex = regexp.MustCompile(`^#[a-fA-F0-9]{6}$`)
	// You can only label issues and pulls presently
	validScopes = []string{tangled.RepoIssueNSID, tangled.RepoPullNSID}
)

var _ Validator = new(LabelDefinition)

func (l *LabelDefinition) Validate() error {
	if l.Name == "" {
		return fmt.Errorf("label name is empty")
	}
	if len(l.Name) > 40 {
		return fmt.Errorf("label name too long (max 40 graphemes)")
	}
	if len(l.Name) < 1 {
		return fmt.Errorf("label name too short (min 1 grapheme)")
	}
	if !labelNameRegex.MatchString(l.Name) {
		return fmt.Errorf("label name contains invalid characters (use only letters, numbers, hyphens, and underscores)")
	}

	if !l.ValueType.IsConcreteType() {
		return fmt.Errorf("invalid value type: %q (must be one of: null, boolean, integer, string)", l.ValueType.Type)
	}

	// null type checks: cannot be enums, multiple or explicit format
	if l.ValueType.IsNull() && l.ValueType.IsEnum() {
		return fmt.Errorf("null type cannot be used in conjunction with enum type")
	}
	if l.ValueType.IsNull() && l.Multiple {
		return fmt.Errorf("null type labels cannot be multiple")
	}
	if l.ValueType.IsNull() && !l.ValueType.IsAnyFormat() {
		return fmt.Errorf("format cannot be used in conjunction with null type")
	}

	// format checks: cannot be used with enum, or integers
	if !l.ValueType.IsAnyFormat() && l.ValueType.IsEnum() {
		return fmt.Errorf("enum types cannot be used in conjunction with format specification")
	}

	if !l.ValueType.IsAnyFormat() && !l.ValueType.IsString() {
		return fmt.Errorf("format specifications are only permitted on string types")
	}

	// validate scope (nsid format)
	if l.Scope == nil {
		return fmt.Errorf("scope is required")
	}
	for _, s := range l.Scope {
		if _, err := syntax.ParseNSID(s); err != nil {
			return fmt.Errorf("failed to parse scope: %w", err)
		}
		if !slices.Contains(validScopes, s) {
			return fmt.Errorf("invalid scope: scope must be present in %q", validScopes)
		}
	}

	// validate color if provided
	if l.Color != nil {
		color := strings.TrimSpace(*l.Color)
		if color == "" {
			// empty color is fine, set to nil
			l.Color = nil
		} else {
			if !colorRegex.MatchString(color) {
				return fmt.Errorf("color must be a valid hex color (e.g. #79FFE1 or #000)")
			}
			// expand 3-digit hex to 6-digit hex
			if len(color) == 4 { // #ABC
				color = fmt.Sprintf("#%c%c%c%c%c%c", color[1], color[1], color[2], color[2], color[3], color[3])
			}
			// convert to uppercase for consistency
			color = strings.ToUpper(color)
			l.Color = &color
		}
	}

	return nil
}

// ValidateOperandValue validates the label operation operand value based on
// label definition.
//
// NOTE: This can modify the [LabelOp]
func (def *LabelDefinition) ValidateOperandValue(op *LabelOp) error {
	expectedKey := def.AtUri().String()
	if op.OperandKey != def.AtUri().String() {
		return fmt.Errorf("operand key %q does not match label definition URI %q", op.OperandKey, expectedKey)
	}

	valueType := def.ValueType

	// this is permitted, it "unsets" a label
	if op.OperandValue == "" {
		op.Operation = LabelOperationDel
		return nil
	}

	switch valueType.Type {
	case ConcreteTypeNull:
		// For null type, value should be empty
		if op.OperandValue != "null" {
			return fmt.Errorf("null type requires empty value, got %q", op.OperandValue)
		}

	case ConcreteTypeString:
		// For string type, validate enum constraints if present
		if valueType.IsEnum() {
			if !slices.Contains(valueType.Enum, op.OperandValue) {
				return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum)
			}
		}

		switch valueType.Format {
		case ValueTypeFormatDid:
			if _, err := syntax.ParseDID(op.OperandValue); err != nil {
				return fmt.Errorf("failed to resolve did/handle: %w", err)
			}
		case ValueTypeFormatAny, "":
		default:
			return fmt.Errorf("unsupported format constraint: %q", valueType.Format)
		}

	case ConcreteTypeInt:
		if op.OperandValue == "" {
			return fmt.Errorf("integer type requires non-empty value")
		}
		if _, err := fmt.Sscanf(op.OperandValue, "%d", new(int)); err != nil {
			return fmt.Errorf("value %q is not a valid integer", op.OperandValue)
		}

		if valueType.IsEnum() {
			if !slices.Contains(valueType.Enum, op.OperandValue) {
				return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum)
			}
		}

	case ConcreteTypeBool:
		if op.OperandValue != "true" && op.OperandValue != "false" {
			return fmt.Errorf("boolean type requires value to be 'true' or 'false', got %q", op.OperandValue)
		}

		// validate enum constraints if present (though uncommon for booleans)
		if valueType.IsEnum() {
			if !slices.Contains(valueType.Enum, op.OperandValue) {
				return fmt.Errorf("value %q is not in allowed enum values %v", op.OperandValue, valueType.Enum)
			}
		}

	default:
		return fmt.Errorf("unsupported value type: %q", valueType.Type)
	}

	return nil
}

// random color for a given seed
func randomColor(seed string) string {
	hash := sha1.Sum([]byte(seed))
	hexStr := hex.EncodeToString(hash[:])
	r := hexStr[0:2]
	g := hexStr[2:4]
	b := hexStr[4:6]

	return fmt.Sprintf("#%s%s%s", r, g, b)
}

func (l LabelDefinition) GetColor() string {
	if l.Color == nil {
		seed := fmt.Sprintf("%d:%s:%s", l.Id, l.Did, l.Rkey)
		color := randomColor(seed)
		return color
	}

	return *l.Color
}

func LabelDefinitionFromRecord(did, rkey string, record tangled.LabelDefinition) (*LabelDefinition, error) {
	created, err := time.Parse(time.RFC3339, record.CreatedAt)
	if err != nil {
		created = time.Now()
	}

	multiple := false
	if record.Multiple != nil {
		multiple = *record.Multiple
	}

	var vt ValueType
	if record.ValueType != nil {
		vt = ValueTypeFromRecord(*record.ValueType)
	}

	return &LabelDefinition{
		Did:  did,
		Rkey: rkey,

		Name:      record.Name,
		ValueType: vt,
		Scope:     record.Scope,
		Color:     record.Color,
		Multiple:  multiple,
		Created:   created,
	}, nil
}

type LabelOp struct {
	Id           int64
	Did          string
	Rkey         string
	Subject      syntax.ATURI
	Operation    LabelOperation
	OperandKey   string
	OperandValue string
	PerformedAt  time.Time
	IndexedAt    time.Time
}

func (l LabelOp) SortAt() time.Time {
	createdAt := l.PerformedAt
	indexedAt := l.IndexedAt

	// if we don't have an indexedat, fall back to now
	if indexedAt.IsZero() {
		indexedAt = time.Now()
	}

	// if createdat is invalid (before epoch), treat as null -> return zero time
	if createdAt.Before(time.UnixMicro(0)) {
		return time.Time{}
	}

	// if createdat is <= indexedat, use createdat
	if createdAt.Before(indexedAt) || createdAt.Equal(indexedAt) {
		return createdAt
	}

	// otherwise, createdat is in the future relative to indexedat -> use indexedat
	return indexedAt
}

var _ Validator = new(LabelOp)

func (l *LabelOp) Validate() error {
	if _, err := syntax.ParseATURI(string(l.Subject)); err != nil {
		return fmt.Errorf("invalid subject URI: %w", err)
	}
	if l.Operation != LabelOperationAdd && l.Operation != LabelOperationDel {
		return fmt.Errorf("invalid operation: %q (must be 'add' or 'del')", l.Operation)
	}
	// Validate performed time is not zero/invalid
	if l.PerformedAt.IsZero() {
		return fmt.Errorf("performed_at timestamp is required")
	}
	return nil
}

type LabelOperation string

const (
	LabelOperationAdd LabelOperation = "add"
	LabelOperationDel LabelOperation = "del"
)

// a record can create multiple label ops
func LabelOpsFromRecord(did, rkey string, record tangled.LabelOp) []LabelOp {
	performed, err := time.Parse(time.RFC3339, record.PerformedAt)
	if err != nil {
		performed = time.Now()
	}

	mkOp := func(operand *tangled.LabelOp_Operand) LabelOp {
		return LabelOp{
			Did:          did,
			Rkey:         rkey,
			Subject:      syntax.ATURI(record.Subject),
			OperandKey:   operand.Key,
			OperandValue: operand.Value,
			PerformedAt:  performed,
		}
	}

	var ops []LabelOp
	// deletes first, then additions
	for _, o := range record.Delete {
		if o != nil {
			op := mkOp(o)
			op.Operation = LabelOperationDel
			ops = append(ops, op)
		}
	}
	for _, o := range record.Add {
		if o != nil {
			op := mkOp(o)
			op.Operation = LabelOperationAdd
			ops = append(ops, op)
		}
	}

	return ops
}

func LabelOpsAsRecord(ops []LabelOp) tangled.LabelOp {
	if len(ops) == 0 {
		return tangled.LabelOp{}
	}

	// use the first operation to establish common fields
	first := ops[0]
	record := tangled.LabelOp{
		Subject:     string(first.Subject),
		PerformedAt: first.PerformedAt.Format(time.RFC3339),
	}

	var addOperands []*tangled.LabelOp_Operand
	var deleteOperands []*tangled.LabelOp_Operand

	for _, op := range ops {
		operand := &tangled.LabelOp_Operand{
			Key:   op.OperandKey,
			Value: op.OperandValue,
		}

		switch op.Operation {
		case LabelOperationAdd:
			addOperands = append(addOperands, operand)
		case LabelOperationDel:
			deleteOperands = append(deleteOperands, operand)
		default:
			return tangled.LabelOp{}
		}
	}

	record.Add = addOperands
	record.Delete = deleteOperands

	return record
}

type set = map[string]struct{}

type LabelState struct {
	inner map[string]set
	names map[string]string
}

func NewLabelState() LabelState {
	return LabelState{
		inner: make(map[string]set),
		names: make(map[string]string),
	}
}

func (s LabelState) LabelNames() []string {
	var result []string
	for key, valset := range s.inner {
		if valset == nil {
			continue
		}
		if name, ok := s.names[key]; ok {
			result = append(result, name)
		}
	}
	return result
}

// LabelNameValues returns composite "name:value" strings for all labels
// that have non-empty values.
func (s LabelState) LabelNameValues() []string {
	var result []string
	for key, valset := range s.inner {
		if valset == nil {
			continue
		}
		name, ok := s.names[key]
		if !ok {
			continue
		}
		for val := range valset {
			if val != "" {
				result = append(result, name+":"+val)
			}
		}
	}
	return result
}

func (s LabelState) Inner() map[string]set {
	return s.inner
}

func (s LabelState) SetName(key, name string) {
	s.names[key] = name
}

func (s LabelState) ContainsLabel(l string) bool {
	if valset, exists := s.inner[l]; exists {
		if valset != nil {
			return true
		}
	}

	return false
}

// go maps behavior in templates make this necessary,
// indexing a map and getting `set` in return is apparently truthy
func (s LabelState) ContainsLabelAndVal(l, v string) bool {
	if valset, exists := s.inner[l]; exists {
		if _, exists := valset[v]; exists {
			return true
		}
	}

	return false
}

func (s LabelState) GetValSet(l string) set {
	if valset, exists := s.inner[l]; exists {
		return valset
	} else {
		return make(set)
	}
}

type LabelApplicationCtx struct {
	Defs map[string]*LabelDefinition // labelAt -> labelDef
}

var (
	LabelNoOpError = errors.New("no-op")
)

func (c *LabelApplicationCtx) ApplyLabelOp(state LabelState, op LabelOp) error {
	def, ok := c.Defs[op.OperandKey]
	if !ok {
		// this def was deleted, but an op exists, so we just skip over the op
		return nil
	}

	state.names[op.OperandKey] = def.Name

	switch op.Operation {
	case LabelOperationAdd:
		// if valueset is empty, init it
		if state.inner[op.OperandKey] == nil {
			state.inner[op.OperandKey] = make(set)
		}

		// if valueset is populated & this val alr exists, this labelop is a noop
		if valueSet, exists := state.inner[op.OperandKey]; exists {
			if _, exists = valueSet[op.OperandValue]; exists {
				return LabelNoOpError
			}
		}

		if def.Multiple {
			// append to set
			state.inner[op.OperandKey][op.OperandValue] = struct{}{}
		} else {
			// reset to just this value
			state.inner[op.OperandKey] = set{op.OperandValue: struct{}{}}
		}

	case LabelOperationDel:
		// if label DNE, then deletion is a no-op
		if valueSet, exists := state.inner[op.OperandKey]; !exists {
			return LabelNoOpError
		} else if _, exists = valueSet[op.OperandValue]; !exists { // if value DNE, then deletion is no-op
			return LabelNoOpError
		}

		if def.Multiple {
			// remove from set
			delete(state.inner[op.OperandKey], op.OperandValue)
		} else {
			// reset the entire label
			delete(state.inner, op.OperandKey)
		}

		// if the map becomes empty, then set it to nil, this is just the inverse of add
		if len(state.inner[op.OperandKey]) == 0 {
			state.inner[op.OperandKey] = nil
		}

	}

	return nil
}

func (c *LabelApplicationCtx) ApplyLabelOps(state LabelState, ops []LabelOp) {
	// sort label ops in sort order first
	slices.SortFunc(ops, func(a, b LabelOp) int {
		return a.SortAt().Compare(b.SortAt())
	})

	// apply ops in sequence
	for _, o := range ops {
		_ = c.ApplyLabelOp(state, o)
	}
}

// IsInverse checks if one label operation is the inverse of another
// returns true if one is an add and the other is a delete with the same key and value
func (op1 LabelOp) IsInverse(op2 LabelOp) bool {
	if op1.OperandKey != op2.OperandKey || op1.OperandValue != op2.OperandValue {
		return false
	}

	return (op1.Operation == LabelOperationAdd && op2.Operation == LabelOperationDel) ||
		(op1.Operation == LabelOperationDel && op2.Operation == LabelOperationAdd)
}

// removes pairs of label operations that are inverses of each other
// from the given slice. the function preserves the order of remaining operations.
func ReduceLabelOps(ops []LabelOp) []LabelOp {
	if len(ops) <= 1 {
		return ops
	}

	keep := make([]bool, len(ops))
	for i := range keep {
		keep[i] = true
	}

	for i := range ops {
		if !keep[i] {
			continue
		}

		for j := i + 1; j < len(ops); j++ {
			if !keep[j] {
				continue
			}

			if ops[i].IsInverse(ops[j]) {
				keep[i] = false
				keep[j] = false
				break // move to next i since this one is now eliminated
			}
		}
	}

	// build result slice with only kept operations
	var result []LabelOp
	for i, op := range ops {
		if keep[i] {
			result = append(result, op)
		}
	}

	return result
}

func FetchLabelDefs(r *idresolver.Resolver, aturis []string) ([]LabelDefinition, error) {
	var labelDefs []LabelDefinition
	ctx := context.Background()

	for _, dl := range aturis {
		atUri, err := syntax.ParseATURI(dl)
		if err != nil {
			return nil, fmt.Errorf("failed to parse AT-URI %s: %v", dl, err)
		}
		if atUri.Collection() != tangled.LabelDefinitionNSID {
			return nil, fmt.Errorf("expected AT-URI pointing %s collection: %s", tangled.LabelDefinitionNSID, atUri)
		}

		owner, err := r.ResolveIdent(ctx, atUri.Authority().String())
		if err != nil {
			return nil, fmt.Errorf("failed to resolve default label owner DID %s: %v", atUri.Authority(), err)
		}

		xrpcc := xrpc.Client{
			Host: owner.PDSEndpoint(),
		}

		record, err := atproto.RepoGetRecord(
			ctx,
			&xrpcc,
			"",
			atUri.Collection().String(),
			atUri.Authority().String(),
			atUri.RecordKey().String(),
		)
		if err != nil {
			return nil, fmt.Errorf("failed to get record for %s: %v", atUri, err)
		}

		if record != nil {
			bytes, err := record.Value.MarshalJSON()
			if err != nil {
				return nil, fmt.Errorf("failed to marshal record value for %s: %v", atUri, err)
			}

			raw := json.RawMessage(bytes)
			labelRecord := tangled.LabelDefinition{}
			err = json.Unmarshal(raw, &labelRecord)
			if err != nil {
				return nil, fmt.Errorf("invalid record for %s: %w", atUri, err)
			}

			labelDef, err := LabelDefinitionFromRecord(
				atUri.Authority().String(),
				atUri.RecordKey().String(),
				labelRecord,
			)
			if err != nil {
				return nil, fmt.Errorf("failed to create label definition from record %s: %v", atUri, err)
			}

			labelDefs = append(labelDefs, *labelDef)
		}
	}

	return labelDefs, nil
}
