// Package handlers provides the template renderer and all HTTP handlers.
package handlers

import (
	"bytes"
	"fmt"
	"html/template"
	"io/fs"
	"log"
	"net/http"
	"path"
	"strings"

	"crowdsec-dashy/internal/crowdsec"
	"crowdsec-dashy/internal/middleware"
)

// -----------------------------------------------------------------------
// Renderer
// -----------------------------------------------------------------------

// Renderer holds pre-parsed templates, one per page.
type Renderer struct {
	templates map[string]*template.Template
	funcMap   template.FuncMap
}

// bareLayoutPages lists page names that use base_bare.html instead of base.html.
var bareLayoutPages = map[string]bool{
	"login": true,
}

// NewRenderer parses all page templates. Most pages use the sidebar layout
// (base.html); pages listed in bareLayoutPages use the bare layout (base_bare.html).
func NewRenderer(fsys fs.FS) (*Renderer, error) {
	r := &Renderer{
		templates: make(map[string]*template.Template),
		funcMap:   buildFuncMap(),
	}

	const (
		basePath     = "templates/layouts/base.html"
		barePath     = "templates/layouts/base_bare.html"
	)

	pages, err := fs.Glob(fsys, "templates/pages/*.html")
	if err != nil {
		return nil, fmt.Errorf("renderer: glob pages: %w", err)
	}
	if len(pages) == 0 {
		return nil, fmt.Errorf("renderer: no page templates found")
	}

	for _, page := range pages {
		name := templateName(page)
		layout := basePath
		if bareLayoutPages[name] {
			layout = barePath
		}
		tmpl, err := template.New(path.Base(page)).
			Funcs(r.funcMap).
			ParseFS(fsys, layout, page)
		if err != nil {
			return nil, fmt.Errorf("renderer: parse %s: %w", name, err)
		}
		r.templates[name] = tmpl
		log.Printf("renderer: registered template %q (layout: %s)", name, path.Base(layout))
	}

	return r, nil
}

// Render executes the named template into a buffer then writes to w.
func (r *Renderer) Render(w http.ResponseWriter, name string, data any) {
	tmpl, ok := r.templates[name]
	if !ok {
		http.Error(w, fmt.Sprintf("template %q not found", name), http.StatusInternalServerError)
		return
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, data); err != nil {
		log.Printf("renderer: execute %q: %v", name, err)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	_, _ = buf.WriteTo(w)
}

// RenderError renders the error page.
func (r *Renderer) RenderError(w http.ResponseWriter, code int, msg string) {
	w.WriteHeader(code)
	r.Render(w, "error", ErrorData{
		PageData: PageData{CurrentPath: "/error", Title: fmt.Sprintf("Error %d", code)},
		Code:     code,
		Message:  msg,
	})
}

func templateName(p string) string {
	base := path.Base(p)
	return base[:len(base)-len(path.Ext(base))]
}

// -----------------------------------------------------------------------
// FuncMap
// -----------------------------------------------------------------------

func buildFuncMap() template.FuncMap {
	return template.FuncMap{
		"inc": func(i int) int { return i + 1 },
		"dec": func(i int) int { return i - 1 },
		// dict builds a map for passing multiple values to a sub-template.
		// Usage: {{template "foo" dict "Key1" val1 "Key2" val2}}
		"dict": func(pairs ...any) (map[string]any, error) {
			if len(pairs)%2 != 0 {
				return nil, fmt.Errorf("dict: odd number of arguments")
			}
			m := make(map[string]any, len(pairs)/2)
			for i := 0; i < len(pairs); i += 2 {
				k, ok := pairs[i].(string)
				if !ok {
					return nil, fmt.Errorf("dict: key %v is not a string", pairs[i])
				}
				m[k] = pairs[i+1]
			}
			return m, nil
		},
		// decisionTypeBadge returns a CSS class suffix for a decision type.
		"decisionBadgeClass": func(t string) string {
			switch strings.ToLower(t) {
			case "ban":
				return "badge-red"
			case "captcha":
				return "badge-amber"
			case "throttle":
				return "badge-blue"
			default:
				return "badge-gray"
			}
		},
		// originBadgeClass returns a CSS class for a decision origin.
		"originBadgeClass": func(o string) string {
			switch strings.ToLower(o) {
			case "crowdsec":
				return "badge-cyan"
			case "cscli":
				return "badge-purple"
			case "lists":
				return "badge-green"
			default:
				return "badge-gray"
			}
		},
		// hubStatusClass returns a CSS class for a hub item status.
		"hubStatusClass": func(s string) string {
			switch strings.ToLower(s) {
			case "enabled":
				return "badge-green"
			case "disabled":
				return "badge-gray"
			case "update-available":
				return "badge-amber"
			default:
				return "badge-gray"
			}
		},
		// safeHTML allows raw HTML in templates (use carefully).
		"safeHTML": func(s string) template.HTML {
			return template.HTML(s) //nolint:gosec
		},
		// boolIcon returns a UTF-8 icon for boolean display.
		"boolIcon": func(b bool) string {
			if b {
				return "✓"
			}
			return "✗"
		},
		// truncate shortens a string.
		"truncate": func(s string, n int) string {
			if len(s) <= n {
				return s
			}
			return s[:n] + "…"
		},
		// join joins a string slice.
		"join": strings.Join,
	}
}

// -----------------------------------------------------------------------
// Shared page data
// -----------------------------------------------------------------------

// NavItem describes one sidebar navigation entry.
type NavItem struct {
	Path    string
	Label   string
	Icon    string
	Divider bool // show divider before this item
}

// SidebarNav returns the full navigation definition.
var SidebarNav = []NavItem{
	{Path: "/", Label: "Dashboard", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`},
	{Path: "/decisions", Label: "Decisions", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`, Divider: false},
	{Path: "/alerts", Label: "Alerts", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`},
	{Path: "/bouncers", Label: "Bouncers", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>`, Divider: true},
	{Path: "/machines", Label: "Machines", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>`},
	{Path: "/hub", Label: "Hub", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`, Divider: true},
	{Path: "/metrics-ui", Label: "Metrics", Icon: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>`},
}

// PageData contains fields available to every template.
type PageData struct {
	CurrentPath  string
	Title        string
	Nav          []NavItem
	Flash        FlashMessage
	CLIAvailable bool
	PollInterval int
	CSRFToken    string
}

// FlashMessage is a one-shot notification shown on the next page load.
type FlashMessage struct {
	Type    string // success, error, warning, info
	Message string
}

// ErrorData is passed to the error page template.
type ErrorData struct {
	PageData
	Code    int
	Message string
}

// NewPageData creates PageData from the current request.
func NewPageData(r *http.Request, title string, cliAvail bool, pollSec int) PageData {
	return PageData{
		CurrentPath:  r.URL.Path,
		Title:        title,
		Nav:          SidebarNav,
		CLIAvailable: cliAvail,
		PollInterval: pollSec,
		CSRFToken:    middleware.CSRFFromContext(r),
	}
}

// WithFlash attaches a flash message to PageData.
func (pd PageData) WithFlash(flashType, msg string) PageData {
	pd.Flash = FlashMessage{Type: flashType, Message: msg}
	return pd
}

// -----------------------------------------------------------------------
// Shared handler dependencies
// -----------------------------------------------------------------------

// Deps holds shared dependencies injected into every handler.
type Deps struct {
	Renderer     *Renderer
	LAPI         *crowdsec.LAPIClient
	CLI          *crowdsec.CLIClient
	CLIAvailable bool
	PollInterval int
}
