package llm

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"io/fs"
	"path"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/readonlyfs"
	"gitea.maximumdirect.net/eric/promptkit"
)

type AssetSource struct {
	FS   fs.FS
	Root string
}

type AssetRegistry struct {
	prompts          []AssetSource
	schemas          []AssetSource
	fallbackProfiles []AssetSource
}

type AssetHashPart struct {
	FS   fs.FS
	Path string
}

func NewAssetRegistry() *AssetRegistry {
	return &AssetRegistry{}
}

func (r *AssetRegistry) RegisterPromptFS(fsys fs.FS, root string) error {
	if r == nil {
		return fmt.Errorf("asset registry must not be nil")
	}
	source, err := newAssetSource(fsys, root)
	if err != nil {
		return fmt.Errorf("register prompt assets: %w", err)
	}
	r.prompts = append(r.prompts, source)
	return nil
}

func (r *AssetRegistry) RegisterSchemaFS(fsys fs.FS, root string) error {
	if r == nil {
		return fmt.Errorf("asset registry must not be nil")
	}
	source, err := newAssetSource(fsys, root)
	if err != nil {
		return fmt.Errorf("register schema assets: %w", err)
	}
	r.schemas = append(r.schemas, source)
	return nil
}

// RegisterFallbackProfileFS registers profile assets that PromptKit uses only
// when an operator-configured source does not provide a matching profile.
func (r *AssetRegistry) RegisterFallbackProfileFS(fsys fs.FS, root string) error {
	if r == nil {
		return fmt.Errorf("asset registry must not be nil")
	}
	source, err := newAssetSource(fsys, root)
	if err != nil {
		return fmt.Errorf("register fallback profile assets: %w", err)
	}
	r.fallbackProfiles = append(r.fallbackProfiles, source)
	return nil
}

func (r *AssetRegistry) PromptFS() (fs.FS, error) {
	if r == nil {
		return nil, fmt.Errorf("asset registry must not be nil")
	}
	return flattenAssetSources(r.prompts)
}

func (r *AssetRegistry) SchemaFS() (fs.FS, error) {
	if r == nil {
		return nil, fmt.Errorf("asset registry must not be nil")
	}
	return flattenAssetSources(r.schemas)
}

func (r *AssetRegistry) FallbackProfileFS() (fs.FS, error) {
	if r == nil {
		return nil, fmt.Errorf("asset registry must not be nil")
	}
	return flattenAssetSources(r.fallbackProfiles)
}

// FallbackProfileDigest returns a deterministic, non-secret identity for the
// flattened fallback profile assets.
func (r *AssetRegistry) FallbackProfileDigest() (string, error) {
	_, digest, _, err := r.fallbackProfileAssets()
	return digest, err
}

func (r *AssetRegistry) PromptKitOptions() ([]promptkit.Option, error) {
	options, _, err := r.promptKitOptions()
	return options, err
}

func (r *AssetRegistry) promptKitOptions() ([]promptkit.Option, string, error) {
	promptFS, err := r.PromptFS()
	if err != nil {
		return nil, "", fmt.Errorf("prepare prompt assets: %w", err)
	}
	schemaFS, err := r.SchemaFS()
	if err != nil {
		return nil, "", fmt.Errorf("prepare schema assets: %w", err)
	}
	options := []promptkit.Option{
		promptkit.WithPromptFS(promptFS, "."),
		promptkit.WithSchemaFS(schemaFS, "."),
	}
	fallbackFS, fallbackDigest, hasFallback, err := r.fallbackProfileAssets()
	if err != nil {
		return nil, "", err
	}
	if hasFallback {
		options = append(options, promptkit.WithFallbackProfileFS(fallbackFS, "."))
	}
	return options, fallbackDigest, nil
}

func (r *AssetRegistry) promptKitFallbackProfileOption() (promptkit.Option, bool, error) {
	fallbackFS, _, hasFallback, err := r.fallbackProfileAssets()
	if err != nil {
		return nil, false, err
	}
	if !hasFallback {
		return nil, false, nil
	}
	return promptkit.WithFallbackProfileFS(fallbackFS, "."), true, nil
}

func (r *AssetRegistry) fallbackProfileAssets() (fs.FS, string, bool, error) {
	if r == nil {
		return nil, "", false, fmt.Errorf("asset registry must not be nil")
	}
	if len(r.fallbackProfiles) == 0 {
		empty := sha256.Sum256([]byte("notarius:fallback-profile-assets:empty"))
		return nil, "sha256:" + hex.EncodeToString(empty[:]), false, nil
	}
	fallbackFS, err := r.FallbackProfileFS()
	if err != nil {
		return nil, "", false, fmt.Errorf("prepare fallback profile assets: %w", err)
	}
	digest, err := hashAssetFilesystem(fallbackFS)
	if err != nil {
		return nil, "", false, err
	}
	return fallbackFS, digest, true, nil
}

func HashAssets(parts []AssetHashPart) (string, error) {
	if len(parts) == 0 {
		return "", fmt.Errorf("asset hash requires at least one part")
	}
	hash := sha256.New()
	for _, part := range parts {
		cleanPath, err := cleanAssetPath(part.Path)
		if err != nil {
			return "", fmt.Errorf("hash asset %q: %w", part.Path, err)
		}
		data, err := fs.ReadFile(part.FS, cleanPath)
		if err != nil {
			return "", fmt.Errorf("read hash asset %s: %w", cleanPath, err)
		}
		if _, err := io.WriteString(hash, cleanPath); err != nil {
			return "", err
		}
		if _, err := hash.Write([]byte{0}); err != nil {
			return "", err
		}
		if _, err := hash.Write(data); err != nil {
			return "", err
		}
		if _, err := hash.Write([]byte{0}); err != nil {
			return "", err
		}
	}
	return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
}

func hashAssetFilesystem(fsys fs.FS) (string, error) {
	var parts []AssetHashPart
	err := fs.WalkDir(fsys, ".", func(name string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			return nil
		}
		parts = append(parts, AssetHashPart{FS: fsys, Path: name})
		return nil
	})
	if err != nil {
		return "", fmt.Errorf("walk assets for digest: %w", err)
	}
	if len(parts) > 0 {
		return HashAssets(parts)
	}
	empty := sha256.Sum256([]byte("notarius:fallback-profile-assets:empty"))
	return "sha256:" + hex.EncodeToString(empty[:]), nil
}

func newAssetSource(fsys fs.FS, root string) (AssetSource, error) {
	if fsys == nil {
		return AssetSource{}, fmt.Errorf("filesystem must not be nil")
	}
	cleanRoot, err := cleanAssetRoot(root)
	if err != nil {
		return AssetSource{}, err
	}
	return AssetSource{FS: fsys, Root: cleanRoot}, nil
}

func flattenAssetSources(sources []AssetSource) (fs.FS, error) {
	out := make(map[string][]byte)
	for _, source := range sources {
		if err := fs.WalkDir(source.FS, source.Root, func(name string, entry fs.DirEntry, walkErr error) error {
			if walkErr != nil {
				return walkErr
			}
			if entry.IsDir() {
				return nil
			}
			rel := name
			if source.Root != "." {
				rel = strings.TrimPrefix(name, source.Root+"/")
			}
			rel, err := cleanAssetPath(rel)
			if err != nil {
				return err
			}
			if _, exists := out[rel]; exists {
				return fmt.Errorf("duplicate asset path %q", rel)
			}
			data, err := fs.ReadFile(source.FS, name)
			if err != nil {
				return err
			}
			out[rel] = data
			return nil
		}); err != nil {
			return nil, fmt.Errorf("walk asset root %s: %w", source.Root, err)
		}
	}
	return readonlyfs.New(out)
}

func cleanAssetRoot(root string) (string, error) {
	trimmed := strings.TrimSpace(root)
	if trimmed == "" || trimmed == "." {
		return ".", nil
	}
	return cleanAssetPath(trimmed)
}

func cleanAssetPath(name string) (string, error) {
	trimmed := strings.TrimSpace(name)
	if trimmed == "" {
		return "", fmt.Errorf("path must not be empty")
	}
	cleaned := path.Clean(strings.TrimPrefix(trimmed, "/"))
	if cleaned == "." || !fs.ValidPath(cleaned) {
		return "", fmt.Errorf("invalid path %q", name)
	}
	return cleaned, nil
}
