package httpx

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"html"
	"html/template"
	"io/fs"
	"net/http"
	"net/url"
	"strings"

	"bountyboard/api"
	"bountyboard/internal/domain"
	"bountyboard/web"
)

// pageData is the payload every template receives.
type pageData struct {
	Title        string
	Active       string // nav highlight key
	User         *domain.User
	DefaultTheme string
	NavLayout    string
	Narrow       bool
	OIDCEnabled  bool
	Error        string
	HasLocalAuth bool
	Scripts      []string
	Data         map[string]any // page-specific extras
}

var templateFuncs = template.FuncMap{
	"initials": func(name string) string {
		parts := strings.Fields(name)
		switch {
		case len(parts) >= 2:
			return strings.ToUpper(string([]rune(parts[0])[:1]) + string([]rune(parts[1])[:1]))
		case len(parts) == 1 && len(parts[0]) > 0:
			return strings.ToUpper(string([]rune(parts[0])[:1]))
		default:
			return "?"
		}
	},
}

// parseTemplates builds one template set per page (layout + page content).
func parseTemplates() (map[string]*template.Template, error) {
	pages, err := fs.Glob(web.FS, "templates/*.html")
	if err != nil {
		return nil, err
	}
	out := make(map[string]*template.Template)
	for _, page := range pages {
		name := strings.TrimPrefix(page, "templates/")
		if name == "layout.html" {
			continue
		}
		t, err := template.New(name).Funcs(templateFuncs).
			ParseFS(web.FS, "templates/layout.html", page)
		if err != nil {
			return nil, fmt.Errorf("parse %s: %w", page, err)
		}
		out[name] = t
	}
	return out, nil
}

func (s *Server) render(w http.ResponseWriter, r *http.Request, page string, data *pageData) {
	t, ok := s.templates[page]
	if !ok {
		s.internalError(w, r, "render", fmt.Errorf("unknown template %q", page))
		return
	}
	if data == nil {
		data = &pageData{}
	}
	if data.DefaultTheme == "" {
		data.DefaultTheme = "light"
		if data.User != nil && data.User.Settings.Theme != "" {
			data.DefaultTheme = data.User.Settings.Theme
		}
	}
	if data.NavLayout == "" {
		data.NavLayout = "side" // sidebar by default
		if data.User != nil && data.User.Settings.NavLayout == "top" {
			data.NavLayout = "top"
		}
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := t.ExecuteTemplate(w, "layout", data); err != nil {
		s.log.Error("execute template", "page", page, "err", err)
	}
}

// pageUser resolves the session for an HTML page; returns nil when not
// logged in (no error response — pages redirect instead).
func (s *Server) pageUser(r *http.Request) *domain.User {
	c, err := r.Cookie(sessionCookie)
	if err != nil || c.Value == "" {
		return nil
	}
	sess, err := s.store.SessionByToken(r.Context(), c.Value)
	if err != nil {
		return nil
	}
	u, err := s.store.UserByID(r.Context(), sess.UserID)
	if err != nil || u.Disabled {
		return nil
	}
	return u
}

// page wraps an HTML handler that requires login: redirects anonymous
// visitors to /login?next=… and forces pending password changes through
// /change-password.
func (s *Server) page(h func(http.ResponseWriter, *http.Request, *domain.User)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		u := s.pageUser(r)
		if u == nil {
			http.Redirect(w, r, "/login?next="+url.QueryEscape(r.URL.RequestURI()), http.StatusFound)
			return
		}
		if u.MustChangePassword() && r.URL.Path != "/change-password" {
			http.Redirect(w, r, "/change-password", http.StatusFound)
			return
		}
		h(w, r, u)
	}
}

func (s *Server) routesWeb(mux *http.ServeMux) {
	staticFS, err := fs.Sub(web.FS, "static")
	if err != nil {
		panic(err) // embed layout is fixed at compile time
	}
	// no-cache + a build-wide strong ETag: browsers revalidate on every
	// load (cheap 304) and can never run a stale CSS/JS mix after a deploy
	etag := staticETag(staticFS)
	fileServer := http.StripPrefix("/static/", http.FileServerFS(staticFS))
	mux.Handle("GET /static/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("If-None-Match") == etag {
			w.WriteHeader(http.StatusNotModified)
			return
		}
		w.Header().Set("Cache-Control", "no-cache")
		w.Header().Set("ETag", etag)
		fileServer.ServeHTTP(w, r)
	}))

	mux.HandleFunc("GET /{$}", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
		s.render(w, r, "home.html", &pageData{Title: "Home", User: u})
	}))

	mux.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) {
		if s.pageUser(r) != nil {
			http.Redirect(w, r, "/", http.StatusFound)
			return
		}
		s.render(w, r, "login.html", &pageData{
			Title: "Log in", Narrow: true,
			OIDCEnabled: s.cfg.OIDC.Enabled(),
			Error:       loginErrorMessage(r.URL.Query().Get("error")),
			Scripts:     []string{"/static/js/login.js"},
		})
	})

	mux.HandleFunc("GET /register", func(w http.ResponseWriter, r *http.Request) {
		if s.pageUser(r) != nil {
			http.Redirect(w, r, "/", http.StatusFound)
			return
		}
		s.render(w, r, "register.html", &pageData{
			Title: "Create account", Narrow: true,
			Scripts: []string{"/static/js/register.js"},
		})
	})

	for _, p := range []struct{ path, tmpl, title, script string }{
		{"/forgot-password", "forgot-password.html", "Forgot password", "/static/js/forgot-password.js"},
		{"/reset-password", "reset-password.html", "Reset password", "/static/js/reset-password.js"},
	} {
		mux.HandleFunc("GET "+p.path, func(w http.ResponseWriter, r *http.Request) {
			s.render(w, r, p.tmpl, &pageData{
				Title: p.title, Narrow: true, Scripts: []string{p.script},
			})
		})
	}

	mux.HandleFunc("GET /change-password", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
		s.render(w, r, "change-password.html", &pageData{
			Title: "Change password", User: u, Narrow: true,
			Scripts: []string{"/static/js/change-password.js"},
		})
	}))

	mux.HandleFunc("GET /profile", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
		s.render(w, r, "profile.html", &pageData{
			Title: "Profile", User: u,
			HasLocalAuth: u.Auth.Local != nil,
			Scripts:      []string{"/static/js/profile.js"},
		})
	}))

	mux.HandleFunc("GET /admin", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
		if !u.Roles.Admin {
			s.render(w, r, "placeholder.html", &pageData{Title: "Not authorized", User: u})
			return
		}
		s.render(w, r, "admin.html", &pageData{
			Title: "Administration", Active: "admin", User: u,
			Scripts: []string{"/static/js/admin.js"},
		})
	}))

	mux.HandleFunc("GET /consultant/board", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
		if !u.Roles.Consultant && !u.Roles.Admin {
			s.render(w, r, "placeholder.html", &pageData{Title: "Not authorized", User: u})
			return
		}
		s.render(w, r, "atomization.html", &pageData{
			Title: "Atomization Board", Active: "atomization", User: u,
			Scripts: []string{"/static/js/atomization.js"},
		})
	}))

	rolePage := func(path, tmpl, title, active, script string, check func(*domain.User) bool) {
		mux.HandleFunc("GET "+path, s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
			if check != nil && !check(u) && !u.Roles.Admin {
				s.render(w, r, "placeholder.html", &pageData{Title: "Not authorized", User: u})
				return
			}
			s.render(w, r, tmpl, &pageData{
				Title: title, Active: active, User: u, Scripts: []string{script},
			})
		}))
	}
	isDev := func(u *domain.User) bool { return u.Roles.Developer }
	isCons := func(u *domain.User) bool { return u.Roles.Consultant }
	rolePage("/board", "board.html", "Bounty Board", "board", "/static/js/board.js", isDev)
	rolePage("/my-tasks", "my-tasks.html", "My Tasks", "my-tasks", "/static/js/my-tasks.js", isDev)
	rolePage("/consultant/reviews", "reviews.html", "Review Queue", "reviews", "/static/js/reviews.js", isCons)
	rolePage("/consultant/pool", "pool.html", "Developer Pool", "pool", "/static/js/pool.js", isCons)
	rolePage("/notifications", "notifications.html", "Notifications", "", "/static/js/notifications-page.js", nil)

	mux.HandleFunc("GET /tasks/{id}", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
		s.render(w, r, "task.html", &pageData{
			Title: "Task", User: u,
			Scripts: []string{"/static/js/task.js"},
			Data:    map[string]any{"TaskID": r.PathValue("id")},
		})
	}))

	rolePage("/messages", "messages.html", "Messages", "messages", "/static/js/messages.js", nil)
	rolePage("/metrics", "metrics.html", "Metrics", "metrics", "/static/js/metrics.js", nil)
}

// routesDocs serves the OpenAPI document and a minimal viewer (§6).
func (s *Server) routesDocs(mux *http.ServeMux) {
	mux.HandleFunc("GET /api/openapi.yaml", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/yaml")
		w.Write(api.OpenAPI)
	})
	mux.HandleFunc("GET /api/docs", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Write([]byte(`<!doctype html><html lang="en"><head><meta charset="utf-8">
<title>Bounty Board API</title><link rel="stylesheet" href="/static/css/app.css">
<script src="/static/js/theme.js"></script></head>
<body><main class="container"><h1>Bounty Board API</h1>
<p><a class="btn" href="/api/openapi.yaml" download>Download openapi.yaml</a></p>
<pre style="white-space:pre-wrap;background:var(--surface);border:1px solid var(--border);padding:16px;border-radius:var(--radius)">` +
			html.EscapeString(string(api.OpenAPI)) + `</pre></main></body></html>`))
	})
}

func loginErrorMessage(code string) string {
	switch code {
	case "":
		return ""
	case "oidc_state_mismatch", "oidc_failed":
		return "SSO sign-in failed. Please try again."
	case "oidc_denied":
		return "SSO sign-in was cancelled."
	case "oidc_email_unverified":
		return "Your SSO email address is not verified; ask your identity provider administrator."
	case "oidc_no_email":
		return "Your SSO identity has no email address; one is required."
	case "account_disabled":
		return "This account is disabled."
	default:
		return "Sign-in failed."
	}
}

// staticETag hashes every embedded static asset into one build-wide ETag.
func staticETag(fsys fs.FS) string {
	h := sha256.New()
	fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return err
		}
		data, err := fs.ReadFile(fsys, path)
		if err != nil {
			return err
		}
		h.Write([]byte(path))
		h.Write(data)
		return nil
	})
	return `"` + hex.EncodeToString(h.Sum(nil))[:20] + `"`
}
