package handlers

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// fileIcon holds the two icon filenames for a file type: the standard (modern)
// icon and the retro (Win98) icon. The filenames are resolved against
// static/file-icons/standard and static/file-icons/retro respectively.
type fileIcon struct {
	Standard string `json:"standard"`
	Retro    string `json:"retro"`
}

type iconType struct {
	Mime       string   `json:"mime"`
	Standard   string   `json:"standard"`
	Retro      string   `json:"retro"`
	Extensions []string `json:"extensions"`
}

type iconMapFile struct {
	Default iconType   `json:"default"`
	Types   []iconType `json:"types"`
}

type mimeRule struct {
	pattern string // exact mime ("application/pdf") or major prefix ("image/")
	prefix  bool
	icon    fileIcon
}

// fileIconSet is the loaded icon map: an extension lookup plus content-type
// rules and a fallback. It is built once at startup from icon-map.json.
type fileIconSet struct {
	byExt    map[string]fileIcon
	byMime   []mimeRule
	fallback fileIcon
}

// loadFileIcons reads static/file-icons/icon-map.json and indexes it by
// extension and content type so icons can be assigned at render time.
func loadFileIcons(staticDir string) (*fileIconSet, error) {
	data, err := os.ReadFile(filepath.Join(staticDir, "file-icons", "icon-map.json"))
	if err != nil {
		return nil, err
	}
	var raw iconMapFile
	if err := json.Unmarshal(data, &raw); err != nil {
		return nil, err
	}

	set := &fileIconSet{
		byExt:    make(map[string]fileIcon),
		fallback: fileIcon{Standard: raw.Default.Standard, Retro: raw.Default.Retro},
	}
	if err := validateFileIcon(staticDir, set.fallback); err != nil {
		return nil, err
	}
	for _, t := range raw.Types {
		icon := fileIcon{Standard: t.Standard, Retro: t.Retro}
		if err := validateFileIcon(staticDir, icon); err != nil {
			return nil, err
		}
		for _, ext := range t.Extensions {
			set.byExt[strings.ToLower(strings.TrimPrefix(ext, "."))] = icon
		}
		if t.Mime == "" {
			continue
		}
		if strings.HasSuffix(t.Mime, "/*") {
			set.byMime = append(set.byMime, mimeRule{pattern: strings.TrimSuffix(t.Mime, "*"), prefix: true, icon: icon})
		} else {
			set.byMime = append(set.byMime, mimeRule{pattern: strings.ToLower(t.Mime), icon: icon})
		}
	}
	return set, nil
}

func validateFileIcon(staticDir string, icon fileIcon) error {
	if icon.Standard != "" {
		if err := validateFileIconPath(staticDir, "standard", icon.Standard); err != nil {
			return err
		}
	}
	if icon.Retro != "" {
		if err := validateFileIconPath(staticDir, "retro", icon.Retro); err != nil {
			return err
		}
	}
	return nil
}

func validateFileIconPath(staticDir, theme, name string) error {
	if strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
		return fmt.Errorf("invalid %s file icon path %q", theme, name)
	}
	path := filepath.Join(staticDir, "file-icons", theme, name)
	info, err := os.Stat(path)
	if err != nil {
		return fmt.Errorf("missing %s file icon %q: %w", theme, name, err)
	}
	if info.IsDir() {
		return fmt.Errorf("%s file icon %q is a directory", theme, name)
	}
	return nil
}

// lookup resolves a file's icon from its name (extension) first, falling back to
// its content type, then to the default icon. Extension wins because stored
// content types are often the generic application/octet-stream.
func (s *fileIconSet) lookup(name, contentType string) fileIcon {
	if s == nil {
		return fileIcon{}
	}
	if ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")); ext != "" {
		if icon, ok := s.byExt[ext]; ok {
			return icon
		}
	}

	ct := strings.ToLower(strings.TrimSpace(contentType))
	if i := strings.IndexByte(ct, ';'); i >= 0 {
		ct = strings.TrimSpace(ct[:i])
	}
	if ct != "" && ct != "application/octet-stream" {
		for _, rule := range s.byMime { // exact matches first
			if !rule.prefix && rule.pattern == ct {
				return rule.icon
			}
		}
		for _, rule := range s.byMime { // then major-type prefixes
			if rule.prefix && strings.HasPrefix(ct, rule.pattern) {
				return rule.icon
			}
		}
	}
	return s.fallback
}

// fileIconURL builds the /static URL for an icon filename in the given theme
// directory ("standard" or "retro").
func fileIconURL(theme, name string) string {
	if name == "" {
		return ""
	}
	return "/static/file-icons/" + theme + "/" + name
}
