package handlers

import (
	"fmt"
	"html/template"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"isosilo/internal/iso"
)

type Handler struct {
	dir      string
	tmpl     *template.Template
	staticFS http.FileSystem
}

// breadcrumb represents a link in the navigation chain
type breadcrumb struct {
	Name string
	URL  string
}

func New(dir string) *Handler {
	h := &Handler{dir: dir}
	h.staticFS = http.FS(os.DirFS("static"))
	h.tmpl = template.Must(
		template.New("").Funcs(template.FuncMap{
			"humanSize": humanSize,
			"fileIcon":  fileIcon,
			"urlenc":    url.PathEscape,
			"base":      filepath.Base,
			"add1":      func(i int) int { return i + 1 },
			"trimExt":   func(s string) string { return strings.TrimSuffix(s, filepath.Ext(s)) },
		}).ParseGlob("templates/*.html"),
	)
	return h
}

type libraryEntry struct {
	Name         string
	RelativePath string
	IsDir        bool
	IsISO        bool
	Size         int64
	ModTime      time.Time
	Description  string
	HasImage     bool
	ImageExt     string
}

func (h *Handler) ListISOs(w http.ResponseWriter, r *http.Request) {
	relDir := strings.TrimPrefix(r.URL.Path, "/")
	fullPath := filepath.Join(h.dir, relDir)

	if strings.Contains(relDir, "..") {
		http.Error(w, "Invalid path", http.StatusBadRequest)
		return
	}

	entries, err := os.ReadDir(fullPath)
	if err != nil {
		http.Error(w, "Directory not found", http.StatusNotFound)
		return
	}

	var items []libraryEntry
	for _, e := range entries {
		name := e.Name()
		relPath := filepath.ToSlash(filepath.Join(relDir, name))
		info, _ := e.Info()

		isISO := !e.IsDir() && strings.EqualFold(filepath.Ext(name), ".iso")

		item := libraryEntry{
			Name:         name,
			RelativePath: relPath,
			IsDir:        e.IsDir(),
			IsISO:        isISO,
			ModTime:      info.ModTime(),
			Size:         info.Size(),
		}

		if isISO {
			basePath := filepath.Join(h.dir, relDir, strings.TrimSuffix(name, ".iso"))
			if d, err := os.ReadFile(basePath + ".txt"); err == nil {
				item.Description = string(d)
			}
			for _, ext := range []string{".png", ".jpg", ".jpeg"} {
				if _, err := os.Stat(basePath + ext); err == nil {
					item.HasImage = true
					item.ImageExt = ext
					break
				}
			}
		}

		if item.IsDir || item.IsISO {
			items = append(items, item)
		}
	}

	sort.Slice(items, func(i, j int) bool {
		if items[i].IsDir != items[j].IsDir {
			return items[i].IsDir
		}
		return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
	})

	h.tmpl.ExecuteTemplate(w, "index.html", map[string]any{
		"Title":       "ISO Library",
		"Items":       items,
		"CurrentPath": relDir,
		"Breadcrumbs": buildLibraryBreadcrumbs(relDir),
	})
}

func (h *Handler) BrowseISO(w http.ResponseWriter, r *http.Request) {
	isoPath, internalPath, ok := parsePath(r.URL.Path, "/browse/")
	if !ok {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	reader, err := h.openISO(isoPath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}
	defer reader.Close()

	entries, err := reader.ListDir(internalPath)
	if err != nil {
		http.Error(w, "cannot list directory", http.StatusNotFound)
		return
	}

	sort.Slice(entries, func(i, j int) bool {
		if entries[i].IsDir != entries[j].IsDir {
			return entries[i].IsDir
		}
		return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name)
	})

	h.tmpl.ExecuteTemplate(w, "browse.html", map[string]any{
		"Title":        filepath.Base(isoPath),
		"ISOName":      isoPath,
		"InternalPath": internalPath,
		"Entries":      entries,
		"Breadcrumbs":  buildISOBreadcrumbs(isoPath, internalPath),
	})
}

func (h *Handler) DownloadFile(w http.ResponseWriter, r *http.Request) {
	isoPath, internalPath, ok := parsePath(r.URL.Path, "/file/")
	if !ok || internalPath == "" {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	reader, err := h.openISO(isoPath)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}
	defer reader.Close()

	rc, size, err := reader.FileReader(internalPath)
	if err != nil {
		http.Error(w, "file not found", http.StatusNotFound)
		return
	}
	defer rc.Close()

	filename := filepath.Base(internalPath)
	ext := strings.ToLower(filepath.Ext(filename))
	ct := mime.TypeByExtension(ext)
	if ct == "" {
		ct = "application/octet-stream"
	}

	viewable := false
	switch ext {
	case ".pdf", ".txt", ".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mp3", ".webp":
		viewable = true
	}

	if !viewable {
		w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename=%q`, filename))
	} else {
		w.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename=%q`, filename))
	}

	w.Header().Set("Content-Type", ct)
	w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
	io.Copy(w, rc)
}

func (h *Handler) RawFile(w http.ResponseWriter, r *http.Request) {
	fileName := strings.TrimPrefix(r.URL.Path, "/raw/")
	http.ServeFile(w, r, filepath.Join(h.dir, fileName))
}

func (h *Handler) ServeStatic(w http.ResponseWriter, r *http.Request) {
	// Serve static files with directory listing completely disabled
	filePath := strings.TrimPrefix(r.URL.Path, "/static/")
	if filePath == "" || strings.HasSuffix(filePath, "/") || strings.Contains(filePath, "..") {
		http.NotFound(w, r)
		return
	}

	f, err := h.staticFS.Open(filePath)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer f.Close()

	stat, err := f.Stat()
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if stat.IsDir() {
		http.NotFound(w, r)
		return
	}

	http.ServeContent(w, r, stat.Name(), stat.ModTime(), f)
}

func (h *Handler) openISO(relPath string) (*iso.Reader, error) {
	return iso.Open(filepath.Join(h.dir, relPath))
}

func parsePath(urlPath, prefix string) (isoPath, internalPath string, ok bool) {
	rest := strings.TrimPrefix(urlPath, prefix)
	decoded, _ := url.PathUnescape(rest)
	lower := strings.ToLower(decoded)
	idx := strings.Index(lower, ".iso")
	if idx == -1 {
		return decoded, "", true
	}
	return decoded[:idx+4], strings.TrimPrefix(decoded[idx+4:], "/"), true
}

func buildLibraryBreadcrumbs(relDir string) []breadcrumb {
	crumbs := []breadcrumb{{Name: "Library", URL: "/"}}
	if relDir == "" {
		return crumbs
	}
	acc := ""
	for _, p := range strings.Split(relDir, "/") {
		if p == "" {
			continue
		}
		if acc == "" {
			acc = p
		} else {
			acc = filepath.Join(acc, p)
		}
		crumbs = append(crumbs, breadcrumb{Name: p, URL: "/" + filepath.ToSlash(acc)})
	}
	return crumbs
}

func buildISOBreadcrumbs(isoPath, internalPath string) []breadcrumb {
	parentDir := filepath.Dir(isoPath)
	var crumbs []breadcrumb
	if parentDir == "." {
		crumbs = []breadcrumb{{Name: "Library", URL: "/"}}
	} else {
		crumbs = buildLibraryBreadcrumbs(parentDir)
	}

	crumbs = append(crumbs, breadcrumb{Name: filepath.Base(isoPath), URL: "/browse/" + url.PathEscape(isoPath)})
	if internalPath != "" {
		acc := ""
		for _, p := range strings.Split(internalPath, "/") {
			if p == "" {
				continue
			}
			if acc == "" {
				acc = p
			} else {
				acc += "/" + p
			}
			crumbs = append(crumbs, breadcrumb{Name: p, URL: "/browse/" + url.PathEscape(isoPath) + "/" + acc})
		}
	}
	return crumbs
}

func humanSize(n int64) string {
	if n < 1024 {
		return fmt.Sprintf("%d B", n)
	}
	div, exp := int64(1024), 0
	for v := n / 1024; v >= 1024; v /= 1024 {
		div *= 1024
		exp++
	}
	return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGTPE"[exp])
}

func fileIcon(name string) string {
	ext := strings.ToLower(filepath.Ext(name))
	switch ext {
	case ".iso":
		return "💿"
	case ".pdf":
		return "📄"
	case ".jpg", ".png", ".jpeg":
		return "🖼️"
	case ".txt", ".md":
		return "📝"
	default:
		return "📄"
	}
}
