package plan

import (
	"fmt"
	"sort"
	"strings"
	"time"
)

// inferredSchema retains the constraints that can be proven from an evaluated
// expression. In particular it keeps array item types and represents a string
// literal as a singleton enum, allowing exact enum membership checks without
// treating arbitrary strings as every nominal string-backed type.
func inferredSchema(value any) Schema {
	switch typed := value.(type) {
	case string:
		return guaranteedSchema(Schema{Type: TypeEnum, Values: []string{typed}})
	case bool:
		return guaranteedSchema(Schema{Type: TypeBoolean})
	case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
		return guaranteedSchema(Schema{Type: TypeNumber})
	case map[string]any:
		return guaranteedSchema(Schema{Type: TypeObject})
	case []any:
		array := guaranteedSchema(Schema{Type: TypeArray})
		if len(typed) == 0 {
			return array
		}
		item := inferredSchema(typed[0])
		if item.Type == "" {
			return Schema{}
		}
		for _, value := range typed[1:] {
			item = commonInferredSchema(item, inferredSchema(value))
			if item.Type == "" {
				return Schema{}
			}
		}
		array.Items = &item
		return array
	default:
		return Schema{}
	}
}

func commonInferredSchema(left, right Schema) Schema {
	if left.Type == "" || right.Type == "" {
		return Schema{}
	}
	if left.Type == TypeString && right.Type == TypeEnum || left.Type == TypeEnum && right.Type == TypeString {
		return guaranteedSchema(Schema{Type: TypeString})
	}
	if left.Type != right.Type {
		return Schema{}
	}
	switch left.Type {
	case TypeEnum:
		values := append(append([]string(nil), left.Values...), right.Values...)
		sort.Strings(values)
		unique := values[:0]
		for _, value := range values {
			if len(unique) == 0 || unique[len(unique)-1] != value {
				unique = append(unique, value)
			}
		}
		return guaranteedSchema(Schema{Type: TypeEnum, Values: unique})
	case TypeArray:
		if left.Items == nil || right.Items == nil {
			return guaranteedSchema(Schema{Type: TypeArray})
		}
		items := commonInferredSchema(*left.Items, *right.Items)
		if items.Type == "" {
			return Schema{}
		}
		return guaranteedSchema(Schema{Type: TypeArray, Items: &items})
	default:
		return guaranteedSchema(Schema{Type: left.Type})
	}
}

// schemaAssignable is structural for arrays, set-based for enums, and nominal
// for every other declared type. Enum values are safely assignable to plain
// string, but an unconstrained string is never silently promoted into enum,
// channel, member, agent, scope, duration, or datetime.
func schemaAssignable(source, target Schema) bool {
	// An empty type is already accompanied by a more specific expression or
	// schema diagnostic; avoid cascading a misleading assignment error.
	if source.Type == "" || target.Type == "" {
		return true
	}
	if source.Type == TypeEnum && target.Type == TypeString {
		return true
	}
	// A fixed resolved agent is a valid responder; a responder union is not
	// assignable back to a strict agent target (a selectOne map must never be
	// mistaken for a plain agent string).
	if source.Type == TypeAgent && target.Type == TypeResponder {
		return true
	}
	if source.Type != target.Type {
		return false
	}
	switch source.Type {
	case TypeEnum:
		allowed := make(map[string]bool, len(target.Values))
		for _, value := range target.Values {
			allowed[value] = true
		}
		for _, value := range source.Values {
			if !allowed[value] {
				return false
			}
		}
		return true
	case TypeArray:
		// The empty-array literal has no item observations and is assignable to
		// every well-formed array target.
		if source.Items == nil {
			return true
		}
		return target.Items != nil && schemaAssignable(*source.Items, *target.Items)
	default:
		return true
	}
}

func valueExprAssignable(expression ValueExpr, source, target Schema) bool {
	if source.Type == "" || target.Type == "" {
		return true
	}
	if expression.Value != nil {
		return valueMatchesSchema(*expression.Value, target)
	}
	if schemaRequired(target) && !schemaGuaranteed(source) {
		return false
	}
	return schemaAssignable(source, target)
}

func guaranteedSchema(schema Schema) Schema {
	required := true
	schema.Required = &required
	return schema
}

func describeSchema(schema Schema) string {
	switch schema.Type {
	case TypeEnum:
		return fmt.Sprintf("enum%v", schema.Values)
	case TypeArray:
		if schema.Items == nil {
			return "array<empty>"
		}
		return "array<" + describeSchema(*schema.Items) + ">"
	case "":
		return "unknown type"
	default:
		return string(schema.Type)
	}
}

func waitUntilAssignable(expression ValueExpr, schema Schema) bool {
	if expression.Value != nil {
		text, ok := (*expression.Value).(string)
		return ok && validTemporalLiteral(text)
	}
	if strings.TrimSpace(expression.Ref) != "" {
		return schema.Type == TypeDuration || schema.Type == TypeDatetime
	}
	if schema.Type == TypeDuration || schema.Type == TypeDatetime {
		return true
	}
	// A quoted expression literal is evaluated as a singleton enum by
	// inferredSchema. Do not extend this exception to enum references.
	raw := strings.TrimSpace(expression.Expr)
	if len(raw) >= 2 && (raw[0] == '\'' || raw[0] == '"') && raw[len(raw)-1] == raw[0] && schema.Type == TypeEnum && len(schema.Values) == 1 {
		return validTemporalLiteral(schema.Values[0])
	}
	return false
}

func validTemporalLiteral(value string) bool {
	if duration, err := parsePlanDuration(value); err == nil && duration > 0 {
		return true
	}
	_, err := time.Parse(time.RFC3339, value)
	return err == nil
}
