package main

import (
	"context"
	"embed"
	"encoding/json"
	"fmt"
	"image/png"
	"io"
	"io/fs"
	"log/slog"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"

	"streamdeck-lets-go/internal/config"
)

//go:embed static/*
var staticFS embed.FS

var staticRoot fs.FS

func init() {
	var err error
	staticRoot, err = fs.Sub(staticFS, "static")
	if err != nil {
		panic(fmt.Sprintf("static embed: %v", err))
	}
}

type WebServer struct {
	cfg        *config.Config
	configPath string
	pm         *PageManager
	extraPMs   []*PageManager
	decks      []*Deck
	mu         sync.RWMutex

	sseClients map[chan string]struct{}
	sseMu      sync.RWMutex
}

func NewWebServer(cfg *config.Config, configPath string) *WebServer {
	return &WebServer{
		cfg:        cfg,
		configPath: configPath,
		sseClients: make(map[chan string]struct{}),
	}
}

func (s *WebServer) UpdateConfig(cfg *config.Config) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.cfg = cfg
}

func (s *WebServer) SetPageManager(pm *PageManager) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.pm = pm
}

func (s *WebServer) SetDecks(decks []*Deck) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.decks = decks
}

func (s *WebServer) SetExtraPageManagers(pms []*PageManager) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.extraPMs = pms
}

func (s *WebServer) BroadcastPageChange(page string) {
	s.sseMu.RLock()
	defer s.sseMu.RUnlock()
	for ch := range s.sseClients {
		select {
		case ch <- page:
		default:
		}
	}
}

func (s *WebServer) Serve(ctx context.Context, addr string) error {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /api/config", s.handleGetConfig)
	mux.HandleFunc("PUT /api/config", s.handlePutConfig)
	mux.HandleFunc("GET /api/config/download", s.handleDownloadConfig)
	mux.HandleFunc("POST /api/config/restore", s.handleRestoreConfig)

	mux.HandleFunc("GET /api/pages", s.handleGetPages)

	mux.HandleFunc("GET /api/render", s.handleRender)

	mux.HandleFunc("GET /api/display-outputs", s.handleDisplayOutputs)

	mux.HandleFunc("GET /api/backups", s.handleListBackups)
	mux.HandleFunc("GET /api/backups/{filename}", s.handleGetBackup)

	mux.HandleFunc("GET /api/models", s.handleGetModels)

	mux.HandleFunc("GET /api/decks", s.handleGetDecks)

	mux.HandleFunc("GET /api/status", s.handleGetStatus)

	mux.HandleFunc("GET /api/events", s.handleSSE)
	mux.HandleFunc("POST /api/activate-page", s.handleActivatePage)

	mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticRoot))))

	mux.HandleFunc("/", s.handleSPA)

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == "POST" && r.URL.Path == "/api/upload" {
			s.handleUpload(w, r)
			return
		}
		mux.ServeHTTP(w, r)
	})

	srv := &http.Server{Addr: addr, Handler: handler}

	go func() {
		<-ctx.Done()
		slog.Info("shutting down web server")
		srv.Close()
	}()

	slog.Info("web server listening", "addr", addr)
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		return fmt.Errorf("web server: %w", err)
	}
	return nil
}

func (s *WebServer) handleSPA(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET" {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}
	data, err := fs.ReadFile(staticRoot, "index.html")
	if err != nil {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Write(data)
}

func (s *WebServer) handleSSE(w http.ResponseWriter, r *http.Request) {
	flusher, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "streaming not supported", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	ch := make(chan string, 8)

	s.sseMu.Lock()
	s.sseClients[ch] = struct{}{}
	s.sseMu.Unlock()

	notify := r.Context().Done()
	go func() {
		<-notify
		s.sseMu.Lock()
		delete(s.sseClients, ch)
		s.sseMu.Unlock()
	}()

	flusher.Flush()

	for {
		select {
		case <-notify:
			return
		case page := <-ch:
			data, _ := json.Marshal(map[string]string{"page": page})
			fmt.Fprintf(w, "event: page_changed\ndata: %s\n\n", data)
			flusher.Flush()
		}
	}
}

func (s *WebServer) handleActivatePage(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Page string `json:"page"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
		return
	}
	if req.Page == "" {
		http.Error(w, "page is required", http.StatusBadRequest)
		return
	}

	s.mu.RLock()
	allPMs := append([]*PageManager{s.pm}, s.extraPMs...)
	s.mu.RUnlock()

	for _, pm := range allPMs {
		if pm == nil {
			continue
		}
		if err := pm.ActivatePage(req.Page); err != nil {
			slog.Warn("activate page", "page", req.Page, "error", err)
		} else {
			pm.stopPeriodicKeys()
			pm.startPeriodicKeys()
		}
	}

	s.BroadcastPageChange(req.Page)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"status": "ok", "page": req.Page})
}

func (s *WebServer) handleGetConfig(w http.ResponseWriter, r *http.Request) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(s.cfg)
}

func (s *WebServer) handlePutConfig(w http.ResponseWriter, r *http.Request) {
	var newCfg config.Config
	if err := json.NewDecoder(r.Body).Decode(&newCfg); err != nil {
		http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
		return
	}
	if err := newCfg.Validate(); err != nil {
		http.Error(w, fmt.Sprintf("invalid config: %v", err), http.StatusBadRequest)
		return
	}

	path := config.ConfigPath(s.configPath)

	s.mu.Lock()
	oldCfg := s.cfg
	s.cfg = &newCfg
	s.mu.Unlock()

	if err := s.cfg.Save(s.configPath); err != nil {
		s.mu.Lock()
		s.cfg = oldCfg
		s.mu.Unlock()
		slog.Error("save config", "error", err)
		http.Error(w, fmt.Sprintf("save failed: %v", err), http.StatusInternalServerError)
		return
	}

	backupOldConfig(path)
	gcImages(&newCfg)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"status": "saved"})
}

func (s *WebServer) handleDownloadConfig(w http.ResponseWriter, r *http.Request) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	data, err := json.MarshalIndent(s.cfg, "", "  ")
	if err != nil {
		http.Error(w, "serialization error", http.StatusInternalServerError)
		return
	}

	filename := fmt.Sprintf("streamdeck-config-%s.json", time.Now().Format("2006-01-02"))
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
	w.Write(data)
}

func (s *WebServer) handleRestoreConfig(w http.ResponseWriter, r *http.Request) {
	ct := r.Header.Get("Content-Type")
	var newCfg config.Config

	if strings.HasPrefix(ct, "multipart/form-data") {
		if err := r.ParseMultipartForm(10 << 20); err != nil {
			http.Error(w, fmt.Sprintf("parse form: %v", err), http.StatusBadRequest)
			return
		}
		file, _, err := r.FormFile("file")
		if err != nil {
			http.Error(w, fmt.Sprintf("file field 'file' required: %v", err), http.StatusBadRequest)
			return
		}
		defer file.Close()
		if err := json.NewDecoder(file).Decode(&newCfg); err != nil {
			http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
			return
		}
	} else {
		if err := json.NewDecoder(r.Body).Decode(&newCfg); err != nil {
			http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
			return
		}
	}

	if err := newCfg.Validate(); err != nil {
		http.Error(w, fmt.Sprintf("invalid config: %v", err), http.StatusBadRequest)
		return
	}

	path := config.ConfigPath(s.configPath)
	backupOldConfig(path)

	s.mu.Lock()
	s.cfg = &newCfg
	s.mu.Unlock()

	if err := s.cfg.Save(s.configPath); err != nil {
		http.Error(w, fmt.Sprintf("save failed: %v", err), http.StatusInternalServerError)
		return
	}
	gcImages(&newCfg)

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"status": "restored"})
}

func (s *WebServer) handleGetPages(w http.ResponseWriter, r *http.Request) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	type pageInfo struct {
		Name          string                `json:"name"`
		Keys          []config.KeyConfig    `json:"keys"`
		Icon          string                `json:"icon,omitempty"`
		DynamicKeys   *config.DynamicKeyGen `json:"dynamic_keys,omitempty"`
		Background    string                `json:"background,omitempty"`
		EffectiveKeys []config.KeyConfig    `json:"effective_keys,omitempty"`
	}

	pages := make([]pageInfo, 0, len(s.cfg.Pages))
	for _, p := range s.cfg.Pages {
		pi := pageInfo{
			Name:        p.Name,
			Keys:        p.Keys,
			Icon:        p.Icon,
			DynamicKeys: p.DynamicKeys,
			Background:  p.Background,
		}
		if p.DynamicKeys != nil && s.pm != nil {
			pi.EffectiveKeys = s.pm.GetEffectiveKeys(p.Name)
		}
		pages = append(pages, pi)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(pages)
}

func (s *WebServer) handleRender(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()

	keySize := 72
	if s := q.Get("key_size"); s != "" {
		fmt.Sscanf(s, "%d", &keySize)
	}

	kc := &config.KeyConfig{}

	if page := q.Get("page"); page != "" {
		key := q.Get("key")
		s.mu.RLock()
		for _, p := range s.cfg.Pages {
			if p.Name == page {
				for _, k := range p.Keys {
					if fmt.Sprintf("%d", k.Index) == key {
						kc = &k
						break
					}
				}
				break
			}
		}
		s.mu.RUnlock()
	}

	if icon := q.Get("icon"); icon != "" {
		kc.Icon = icon
		if s := q.Get("icon_scale"); s != "" {
			v := 0.0
			fmt.Sscanf(s, "%f", &v)
			kc.IconScale = &v
		}
	}
	if label := q.Get("label"); label != "" {
		kc.Label = label
	}
	if fs := q.Get("font_size"); fs != "" {
		v := 0.0
		fmt.Sscanf(fs, "%f", &v)
		kc.FontSize = &v
	}
	if bg := q.Get("background"); bg != "" {
		kc.Background = bg
	}

	img := RenderKeyToImage(kc, keySize, s.cfg.ShowLabelBackground)

	w.Header().Set("Content-Type", "image/png")
	w.Header().Set("Cache-Control", "no-cache")
	png.Encode(w, img)
}

func (s *WebServer) handleDisplayOutputs(w http.ResponseWriter, r *http.Request) {
	s.mu.RLock()
	pm := s.pm
	s.mu.RUnlock()

	if pm == nil {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte("{}"))
		return
	}

	outputs := pm.GetDisplayOutputs()
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(outputs)
}

func backupOldConfig(path string) {
	backupDir := filepath.Join(filepath.Dir(path), "backups")
	if err := os.MkdirAll(backupDir, 0755); err != nil {
		slog.Warn("backup: create dir", "error", err)
		return
	}

	data, err := os.ReadFile(path)
	if err != nil {
		return
	}

	name := fmt.Sprintf("config.%s.json", time.Now().Format("2006-01-02T15-04-05"))
	dst := filepath.Join(backupDir, name)
	if err := os.WriteFile(dst, data, 0644); err != nil {
		slog.Warn("backup: write", "error", err)
		return
	}

	entries, _ := os.ReadDir(backupDir)
	if len(entries) > 10 {
		sort.Slice(entries, func(i, j int) bool {
			fi, _ := entries[i].Info()
			fj, _ := entries[j].Info()
			return fi.ModTime().Before(fj.ModTime())
		})
		for _, e := range entries[:len(entries)-10] {
			os.Remove(filepath.Join(backupDir, e.Name()))
		}
	}
}

func collectIcons(cfg *config.Config, used map[string]bool) {
	for _, p := range cfg.Pages {
		for _, k := range p.Keys {
			if k.Icon != "" {
				used[k.Icon] = true
			}
		}
	}
}

func gcImages(cfg *config.Config) {
	imgDir := filepath.Join(configDir(), "images")
	entries, err := os.ReadDir(imgDir)
	if err != nil {
		return
	}

	used := make(map[string]bool)
	collectIcons(cfg, used)

	backupDir := filepath.Join(configDir(), "backups")
	backupEntries, _ := os.ReadDir(backupDir)
	for _, be := range backupEntries {
		if be.IsDir() {
			continue
		}
		data, err := os.ReadFile(filepath.Join(backupDir, be.Name()))
		if err != nil {
			continue
		}
		var backupCfg config.Config
		if err := json.Unmarshal(data, &backupCfg); err != nil {
			continue
		}
		collectIcons(&backupCfg, used)
	}

	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		path := filepath.Join(imgDir, e.Name())
		if !used[path] {
			if err := os.Remove(path); err != nil {
				slog.Warn("gc: remove orphan image", "path", path, "error", err)
			} else {
				slog.Debug("gc: removed orphan image", "path", path)
			}
		}
	}
}

func (s *WebServer) handleListBackups(w http.ResponseWriter, r *http.Request) {
	path := config.ConfigPath(s.configPath)
	backupDir := filepath.Join(filepath.Dir(path), "backups")

	entries, err := os.ReadDir(backupDir)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode([]map[string]interface{}{})
		return
	}

	type backupInfo struct {
		Name string `json:"name"`
		Size int64  `json:"size"`
		Time string `json:"time"`
	}

	var backups []backupInfo
	for _, e := range entries {
		if e.IsDir() || !strings.HasPrefix(e.Name(), "config.") || !strings.HasSuffix(e.Name(), ".json") {
			continue
		}
		fi, err := e.Info()
		if err != nil {
			continue
		}
		backups = append(backups, backupInfo{
			Name: e.Name(),
			Size: fi.Size(),
			Time: fi.ModTime().Format(time.RFC3339),
		})
	}

	sort.Slice(backups, func(i, j int) bool {
		return backups[i].Time > backups[j].Time
	})

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(backups)
}

func (s *WebServer) handleGetBackup(w http.ResponseWriter, r *http.Request) {
	filename := r.PathValue("filename")
	if filename == "" || strings.Contains(filename, "/") || strings.Contains(filename, "..") {
		http.Error(w, "invalid filename", http.StatusBadRequest)
		return
	}

	path := config.ConfigPath(s.configPath)
	backupDir := filepath.Join(filepath.Dir(path), "backups")
	fullPath := filepath.Join(backupDir, filename)

	if !strings.HasPrefix(fullPath, backupDir) {
		http.Error(w, "invalid path", http.StatusBadRequest)
		return
	}

	data, err := os.ReadFile(fullPath)
	if err != nil {
		http.Error(w, "backup not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
	w.Write(data)
}

func (s *WebServer) handleGetModels(w http.ResponseWriter, r *http.Request) {
	type modelInfo struct {
		PID    uint16 `json:"pid"`
		Name   string `json:"name"`
		KeysX  int    `json:"keys_x"`
		KeysY  int    `json:"keys_y"`
		KeySize int   `json:"key_size"`
	}

	models := make([]modelInfo, 0, len(knownDecks))
	for _, d := range knownDecks {
		models = append(models, modelInfo{
			PID:     d.PID,
			Name:    d.Name,
			KeysX:   d.KeysX,
			KeysY:   d.KeysY,
			KeySize: d.KeySize,
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(models)
}

func (s *WebServer) handleGetDecks(w http.ResponseWriter, r *http.Request) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	infos := make([]DeckInfo, 0, len(s.decks))
	for _, d := range s.decks {
		infos = append(infos, d.DeckInfo())
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(infos)
}

func (s *WebServer) handleGetStatus(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"config_version": s.cfg.Version,
		"default_page":   s.cfg.DefaultPage,
		"page_count":     len(s.cfg.Pages),
		"device_count":   len(s.cfg.Devices),
	})
}

func (s *WebServer) handleUpload(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
		return
	}

	if err := r.ParseMultipartForm(10 << 20); err != nil {
		http.Error(w, fmt.Sprintf("parse form: %v", err), http.StatusBadRequest)
		return
	}

	file, handler, err := r.FormFile("file")
	if err != nil {
		http.Error(w, fmt.Sprintf("file field 'file' required: %v", err), http.StatusBadRequest)
		return
	}
	defer file.Close()

	uploadDir := filepath.Join(configDir(), "images")
	if err := os.MkdirAll(uploadDir, 0755); err != nil {
		http.Error(w, fmt.Sprintf("create upload dir: %v", err), http.StatusInternalServerError)
		return
	}

	ext := filepath.Ext(handler.Filename)
	if ext == "" {
		ext = ".png"
	}
	filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
	savePath := filepath.Join(uploadDir, filename)

	dst, err := os.Create(savePath)
	if err != nil {
		http.Error(w, fmt.Sprintf("create file: %v", err), http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	if _, err := io.Copy(dst, file); err != nil {
		http.Error(w, fmt.Sprintf("write file: %v", err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]string{"path": savePath})
}
