package presets

import (
	"embed"
	"errors"
	"fmt"
	"path"
	"sort"
	"strings"
	"sync"

	"gopkg.in/yaml.v3"
)

// RolePreset is one localized built-in Role preset (catalog entry plus its
// resolved definition and attachment manifests).
type RolePreset struct {
	ID           string
	Version      int
	Locale       string
	Name         string
	Description  string
	Scenario     string
	Icon         string
	Tags         []string
	Definition   string
	RuntimeHints RolePresetRuntimeHints
	Attachments  []RolePresetAttachment
}

// RolePresetRuntimeHints captures the runtime suggestion fields of a preset.
type RolePresetRuntimeHints struct {
	Runtime         string
	Model           string
	ReasoningEffort string
	SuggestedName   string
}

// RolePresetAttachment is the catalog-shaped attachment manifest entry of a
// preset. It is a subset of meshx's roles.Attachment — without transport-only
// fields like Metadata — so meshx adapts it at the boundary.
type RolePresetAttachment struct {
	Type        string
	Name        string
	Description string
	Required    bool
	Realization string
	ResourceID  string
	AccessLevel string
	Branch      string
	Content     string
}

type rolePresetCatalogFile struct {
	Presets []rolePresetCatalogEntry `yaml:"presets"`
}

type rolePresetCatalogEntry struct {
	ID           string                           `yaml:"id"`
	Version      int                              `yaml:"version"`
	Scenario     string                           `yaml:"scenario"`
	Icon         string                           `yaml:"icon"`
	Tags         []string                         `yaml:"tags"`
	RuntimeHints rolePresetCatalogRuntimeHints    `yaml:"runtime_hints"`
	Attachments  []rolePresetCatalogAttachment    `yaml:"attachments"`
	Locales      map[string]rolePresetCatalogText `yaml:"locales"`
}

type rolePresetCatalogRuntimeHints struct {
	Runtime         string `yaml:"runtime"`
	Model           string `yaml:"model"`
	ReasoningEffort string `yaml:"reasoning_effort"`
	SuggestedName   string `yaml:"suggested_name"`
}

type rolePresetCatalogAttachment struct {
	Type        string `yaml:"type"`
	Name        string `yaml:"name"`
	Description string `yaml:"description"`
	Required    bool   `yaml:"required"`
	Realization string `yaml:"realization"`
	ResourceID  string `yaml:"resource_id"`
	AccessLevel string `yaml:"access_level"`
	Branch      string `yaml:"branch"`
	Content     string `yaml:"content"`
	LibrarySlug string `yaml:"library_slug"`
	SkillSlug   string `yaml:"skill_slug"`
}

type rolePresetCatalogText struct {
	Name           string                        `yaml:"name"`
	Description    string                        `yaml:"description"`
	DefinitionPath string                        `yaml:"definition_path"`
	Attachments    []rolePresetCatalogAttachment `yaml:"attachments"`
}

//go:embed assets/roles
var rolePresetCatalogFS embed.FS

const rolePresetCatalogFilePath = "assets/roles/catalog.yaml"

var (
	rolePresetCatalogOnce   sync.Once
	rolePresetCatalogCached []RolePreset
	rolePresetCatalogErr    error
)

// RolePresets returns the localized built-in Role presets, de-duplicated by
// preset ID with locale fallback (preferred → family → en-US), sorted by ID.
func RolePresets(locale string) ([]RolePreset, error) {
	templates, err := loadRolePresetCatalog()
	if err != nil {
		return nil, err
	}
	family, preferred := normalizeRolePresetLocale(locale)
	byID := map[string]RolePreset{}
	for _, preset := range templates {
		if preset.Locale == preferred {
			byID[preset.ID] = preset
		}
	}
	for _, preset := range templates {
		if _, ok := byID[preset.ID]; ok {
			continue
		}
		if strings.HasPrefix(strings.ToLower(preset.Locale), family+"-") {
			byID[preset.ID] = preset
		}
	}
	for _, preset := range templates {
		if _, ok := byID[preset.ID]; ok {
			continue
		}
		if preset.Locale == "en-US" {
			byID[preset.ID] = preset
		}
	}
	ids := make([]string, 0, len(byID))
	for id := range byID {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	out := make([]RolePreset, 0, len(ids))
	for _, id := range ids {
		out = append(out, cloneRolePresetForCaller(byID[id]))
	}
	return out, nil
}

// RolePresetByID returns one localized preset by ID. The boolean is false
// (with a nil error) when the ID is not present in the catalog.
func RolePresetByID(id, locale string) (RolePreset, bool, error) {
	id = strings.TrimSpace(id)
	selected, err := RolePresets(locale)
	if err != nil {
		return RolePreset{}, false, err
	}
	for _, preset := range selected {
		if preset.ID == id {
			return preset, true, nil
		}
	}
	return RolePreset{}, false, nil
}

func loadRolePresetCatalog() ([]RolePreset, error) {
	rolePresetCatalogOnce.Do(func() {
		raw, err := rolePresetCatalogFS.ReadFile(rolePresetCatalogFilePath)
		if err != nil {
			rolePresetCatalogErr = fmt.Errorf("read role preset catalog: %w", err)
			return
		}
		rolePresetCatalogCached, rolePresetCatalogErr = parseRolePresetCatalog(raw)
	})
	return rolePresetCatalogCached, rolePresetCatalogErr
}

func parseRolePresetCatalog(raw []byte) ([]RolePreset, error) {
	var file rolePresetCatalogFile
	if err := yaml.Unmarshal(raw, &file); err != nil {
		return nil, fmt.Errorf("parse role preset catalog: %w", err)
	}
	templates := make([]RolePreset, 0, len(file.Presets)*2)
	seen := map[string]struct{}{}
	for _, entry := range file.Presets {
		entry.ID = strings.TrimSpace(entry.ID)
		if entry.ID == "" {
			return nil, errors.New("role preset id is required")
		}
		if entry.Version <= 0 {
			return nil, fmt.Errorf("role preset %q version must be positive", entry.ID)
		}
		if len(entry.Locales) == 0 {
			return nil, fmt.Errorf("role preset %q must define at least one locale", entry.ID)
		}
		for locale, text := range entry.Locales {
			locale = strings.TrimSpace(locale)
			key := entry.ID + "\x00" + locale
			if _, ok := seen[key]; ok {
				return nil, fmt.Errorf("duplicate role preset locale %q for %q", locale, entry.ID)
			}
			seen[key] = struct{}{}
			template, err := rolePresetFromCatalogEntry(entry, locale, text)
			if err != nil {
				return nil, err
			}
			templates = append(templates, template)
		}
	}
	return templates, nil
}

func rolePresetFromCatalogEntry(entry rolePresetCatalogEntry, locale string, text rolePresetCatalogText) (RolePreset, error) {
	if strings.TrimSpace(locale) == "" {
		return RolePreset{}, fmt.Errorf("role preset %q locale is required", entry.ID)
	}
	if strings.TrimSpace(text.Name) == "" {
		return RolePreset{}, fmt.Errorf("role preset %q locale %q name is required", entry.ID, locale)
	}
	definition, err := readRolePresetDefinition(entry.ID, locale, text.DefinitionPath)
	if err != nil {
		return RolePreset{}, err
	}
	attachments := entry.Attachments
	if text.Attachments != nil {
		attachments = text.Attachments
	}
	resolvedAttachments, err := rolePresetAttachmentsFromCatalog(locale, attachments)
	if err != nil {
		return RolePreset{}, fmt.Errorf("role preset %q locale %q attachments: %w", entry.ID, locale, err)
	}
	return RolePreset{
		ID:          entry.ID,
		Version:     entry.Version,
		Locale:      locale,
		Name:        strings.TrimSpace(text.Name),
		Description: strings.TrimSpace(text.Description),
		Scenario:    strings.TrimSpace(entry.Scenario),
		Icon:        strings.TrimSpace(entry.Icon),
		Tags:        normalizeRolePresetStringSlice(entry.Tags),
		RuntimeHints: RolePresetRuntimeHints{
			Runtime:         strings.TrimSpace(entry.RuntimeHints.Runtime),
			Model:           strings.TrimSpace(entry.RuntimeHints.Model),
			ReasoningEffort: strings.TrimSpace(entry.RuntimeHints.ReasoningEffort),
			SuggestedName:   strings.TrimSpace(entry.RuntimeHints.SuggestedName),
		},
		Definition:  definition,
		Attachments: resolvedAttachments,
	}, nil
}

func readRolePresetDefinition(id, locale, definitionPath string) (string, error) {
	definitionPath = strings.TrimSpace(definitionPath)
	if definitionPath == "" {
		return "", fmt.Errorf("role preset %q locale %q definition_path is required", id, locale)
	}
	cleanPath := path.Clean(definitionPath)
	if cleanPath != definitionPath || strings.HasPrefix(cleanPath, "../") || strings.HasPrefix(cleanPath, "/") {
		return "", fmt.Errorf("role preset %q locale %q definition_path %q is invalid", id, locale, definitionPath)
	}
	raw, err := rolePresetCatalogFS.ReadFile(cleanPath)
	if err != nil {
		return "", fmt.Errorf("read role preset %q locale %q definition: %w", id, locale, err)
	}
	definition := strings.TrimSpace(string(raw))
	if definition == "" {
		return "", fmt.Errorf("role preset %q locale %q definition is required", id, locale)
	}
	return definition, nil
}

func rolePresetAttachmentsFromCatalog(locale string, attachments []rolePresetCatalogAttachment) ([]RolePresetAttachment, error) {
	if len(attachments) == 0 {
		return nil, nil
	}
	out := make([]RolePresetAttachment, 0, len(attachments))
	for _, attachment := range attachments {
		librarySlug := strings.TrimSpace(attachment.LibrarySlug)
		skillSlug := strings.TrimSpace(attachment.SkillSlug)
		if librarySlug != "" || skillSlug != "" {
			resolved, err := rolePresetSkillAttachmentFromCatalog(locale, attachment, librarySlug, skillSlug)
			if err != nil {
				return nil, err
			}
			out = append(out, resolved)
			continue
		}
		out = append(out, RolePresetAttachment{
			Type:        strings.TrimSpace(attachment.Type),
			Name:        strings.TrimSpace(attachment.Name),
			Description: strings.TrimSpace(attachment.Description),
			Required:    attachment.Required,
			Realization: strings.TrimSpace(attachment.Realization),
			ResourceID:  strings.TrimSpace(attachment.ResourceID),
			AccessLevel: strings.TrimSpace(attachment.AccessLevel),
			Branch:      strings.TrimSpace(attachment.Branch),
			Content:     strings.TrimSpace(attachment.Content),
		})
	}
	return out, nil
}

func rolePresetSkillAttachmentFromCatalog(locale string, attachment rolePresetCatalogAttachment, librarySlug, skillSlug string) (RolePresetAttachment, error) {
	if librarySlug == "" || skillSlug == "" {
		return RolePresetAttachment{}, errors.New("role preset skill attachment requires library_slug and skill_slug")
	}
	if attachmentType := strings.TrimSpace(attachment.Type); attachmentType != "" && attachmentType != "skill" {
		return RolePresetAttachment{}, fmt.Errorf("role preset skill reference %q/%q must use type skill", librarySlug, skillSlug)
	}
	content, err := OfficialSkillContent(librarySlug, skillSlug, locale)
	if err != nil {
		return RolePresetAttachment{}, err
	}
	realization := strings.TrimSpace(attachment.Realization)
	if realization == "" {
		realization = "copy"
	}
	if realization != "copy" {
		return RolePresetAttachment{}, fmt.Errorf("role preset skill reference %q/%q must use realization copy", librarySlug, skillSlug)
	}
	return RolePresetAttachment{
		Type:        "skill",
		Name:        skillSlug,
		Description: content.Description,
		Required:    attachment.Required,
		Realization: realization,
		Content:     content.Content,
	}, nil
}

func normalizeRolePresetStringSlice(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	out := make([]string, 0, len(values))
	seen := map[string]struct{}{}
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	if len(out) == 0 {
		return nil
	}
	sort.Strings(out)
	return out
}

// cloneRolePresetForCaller returns a copy of preset with its slice fields
// (Tags, Attachments) backed by fresh arrays. The cached catalog parse shares
// slice backing arrays across calls; without this clone, a caller mutating
// preset.Tags[0] or preset.Attachments[0].Name would corrupt the process-level
// cache and leak into subsequent RolePresets / RolePresetByID calls.
func cloneRolePresetForCaller(preset RolePreset) RolePreset {
	preset.Tags = cloneRolePresetStringSlice(preset.Tags)
	preset.Attachments = cloneRolePresetAttachments(preset.Attachments)
	return preset
}

func cloneRolePresetStringSlice(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	out := make([]string, len(values))
	copy(out, values)
	return out
}

func cloneRolePresetAttachments(attachments []RolePresetAttachment) []RolePresetAttachment {
	if len(attachments) == 0 {
		return nil
	}
	out := make([]RolePresetAttachment, len(attachments))
	copy(out, attachments)
	return out
}

func normalizeRolePresetLocale(locale string) (family, preferred string) {
	preferred = strings.TrimSpace(locale)
	if preferred == "" {
		preferred = "en-US"
	}
	family = strings.ToLower(preferred)
	if idx := strings.Index(family, "-"); idx >= 0 {
		family = family[:idx]
	}
	if family == "" {
		family = "en"
	}
	return family, preferred
}
