package shared

import (
	"fmt"
	"io/fs"
	"strings"

	rootassets "gitea.maximumdirect.net/eric/notarius/assets"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
	"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs"
)

// PromptAssetManifest is the ordered set of assets that make up one prompt.
// Module files are addressed in the owning module filesystem. SharedFiles use
// names in sharedPromptPaths, while ExternalSharedFiles are explicit
// caller-owned descriptors. Both kinds are mounted beneath sharedassets.
type PromptAssetManifest struct {
	ModuleDir           string
	ModuleFiles         []promptfs.ModulePromptFile
	SharedFiles         []string
	ExternalSharedFiles []promptfs.SharedPromptFile
}

var sharedPromptPaths = map[string]string{
	"common-dnd-system.md":              "prompts/common-dnd-system.md",
	"common-dnd-extraction-evidence.md": "prompts/common-dnd-extraction-evidence.md",
	"common-dnd-identity.md":            "prompts/common-dnd-identity.md",
	"common-dnd-transcript-full.md":     "prompts/common-dnd-transcript-full.md",
	"common-dnd-transcript-chunk.md":    "prompts/common-dnd-transcript-chunk.md",
	"common-dnd-scene-combat-policy.md": "prompts/common-dnd-scene-combat-policy.md",
	"common-dnd-references.md":          "prompts/common-dnd-references.md",
	"common-dnd-npc-registry.md":        "prompts/common-dnd-npc-registry.md",
}

func sharedAssetFS() (fs.FS, error) {
	assets, err := fs.Sub(rootassets.FS(), "dnd/shared")
	if err != nil {
		return nil, fmt.Errorf("scope shared D&D prompt assets: %w", err)
	}
	return assets, nil
}

func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) {
	sharedFiles, err := manifest.sharedPromptFiles()
	if err != nil {
		return nil, err
	}
	moduleFiles := append([]promptfs.ModulePromptFile(nil), manifest.ModuleFiles...)
	return promptfs.ModulePromptFS(manifest.ModuleDir, moduleFS, moduleFiles, sharedFiles...)
}

func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) {
	sharedFiles, err := manifest.sharedPromptFiles()
	if err != nil {
		return "", err
	}
	moduleFiles := append([]promptfs.ModulePromptFile(nil), manifest.ModuleFiles...)
	if _, err := promptfs.ModulePromptFS(manifest.ModuleDir, moduleFS, moduleFiles, sharedFiles...); err != nil {
		return "", err
	}
	parts := make([]llm.AssetHashPart, 0, len(manifest.ModuleFiles)+len(sharedFiles))
	for _, file := range manifest.ModuleFiles {
		parts = append(parts, llm.AssetHashPart{FS: moduleFS, Path: file.Path})
	}
	for _, file := range sharedFiles {
		parts = append(parts, llm.AssetHashPart{FS: file.FS, Path: file.Path})
	}
	return llm.HashAssets(parts)
}

func (manifest PromptAssetManifest) sharedPromptFiles() ([]promptfs.SharedPromptFile, error) {
	files, err := resolveSharedPromptFiles(manifest.SharedFiles)
	if err != nil {
		return nil, err
	}
	return append(files, manifest.ExternalSharedFiles...), nil
}

func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) {
	assets, err := sharedAssetFS()
	if err != nil {
		return nil, err
	}

	files := make([]promptfs.SharedPromptFile, 0, len(names))
	seen := make(map[string]struct{}, len(names))
	for _, name := range names {
		if name == "" {
			return nil, fmt.Errorf("shared prompt asset name must not be empty")
		}
		if strings.ContainsAny(name, `/\\`) {
			return nil, fmt.Errorf("shared prompt asset name %q must not contain path separators", name)
		}
		if _, ok := seen[name]; ok {
			return nil, fmt.Errorf("duplicate shared prompt asset name %q", name)
		}
		path, ok := sharedPromptPaths[name]
		if !ok {
			return nil, fmt.Errorf("unknown shared prompt asset name %q", name)
		}
		seen[name] = struct{}{}
		files = append(files, promptfs.SharedPromptFile{
			Name: name,
			FS:   assets,
			Path: path,
		})
	}
	return files, nil
}
