package api

import (
	"bytes"
	"embed"
	"fmt"
	"html/template"
	"io/fs"
	"path"
	"sort"
)

// The console used to be one HTML file holding every screen at once: a television opening
// /admin/logs was still sent the accounts settings editor, the feature grid and the
// recommendation inspector, all hidden. That is why it read as one page wearing twelve
// hats. It is now a shell plus one fragment per page, composed here at start-up, so a page
// carries only its own markup and its own script — and so the rail, the page titles and
// the set of legal URLs all come from adminNav rather than being written out three times.
//
// The no-build, no-CDN rule is unchanged: everything below is embedded in the binary and
// inlined into the response. Nothing is fetched from anywhere.

//go:embed admin/shell.html admin/admin.css admin/core.js admin/pages
var adminAssets embed.FS

// adminNavItem is one destination. Hidden items are reachable and titled but are not in
// the rail — an account's own page belongs to the person it is about, not to a menu.
type adminNavItem struct {
	ID     string
	Label  string
	Title  string
	Intro  string
	Icon   string // the `d` of a single stroked path, drawn on a 24×24 grid
	Hidden bool
}

type adminNavGroup struct {
	Label string
	Items []adminNavItem
}

// adminNav is the console's table of contents and the only place a page is declared.
// Adding a page is an entry here plus admin/pages/<id>.html and admin/pages/<id>.js.
var adminNav = []adminNavGroup{
	{
		Items: []adminNavItem{
			{
				ID: "overview", Label: "Overview", Title: "Overview",
				Intro: "What the gateway is doing right now.",
				Icon:  "M4 13h6V4H4zm0 7h6v-4H4zm10 0h6v-9h-6zm0-16v4h6V4z",
			},
		},
	},
	{
		Label: "People",
		Items: []adminNavItem{
			{
				ID: "accounts", Label: "Users", Title: "Memby users",
				Intro: "Who uses Memby, and the devices they are signed in on.",
				Icon:  "M16 19v-1.5A3.5 3.5 0 0 0 12.5 14h-5A3.5 3.5 0 0 0 4 17.5V19M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm7-2h3m-1.5-1.5v3",
			},
			{
				ID: "account", Title: "User", Hidden: true,
				Intro: "Devices, recommendation setup and synced settings for one person.",
			},
			{
				ID: "settings-history", Title: "Settings history", Hidden: true,
				Intro: "Every change to one person's synced settings, and which devices took it.",
			},
			{
				ID: "clients", Label: "Devices", Title: "Devices",
				Intro: "Which sets have reported in, what they are running and what their build understands.",
				Icon:  "M4 5h16v10H4zM9 19h6M12 15v4",
			},
		},
	},
	{
		Label: "Content",
		Items: []adminNavItem{
			{
				ID: "library", Label: "Library", Title: "Library",
				Intro: "Import and inspect the catalogue Memby ranks.",
				Icon:  "M4 5.5h16v13H4zM8 5.5v13M4 10h4",
			},
			{
				ID: "ratings", Label: "Movie ratings", Title: "Movie ratings",
				Intro: "Optional MDBList scores on films and shows.",
				Icon:  "m12 3 2.1 5.4 5.9.4-4.6 3.8 1.5 5.7-4.9-3.2-4.9 3.2 1.5-5.7L4 8.8l5.9-.4L12 3Z",
			},
			{
				ID: "requests", Label: "Media requests", Title: "Media requests",
				Intro: "Who can ask for something the library does not have.",
				Icon:  "M5 4h14v16H5zM8 8h8M8 12h5M15 16h3m-1.5-1.5v3",
			},
		},
	},
	{
		Label: "Personalisation",
		Items: []adminNavItem{
			{
				ID: "recommendations", Label: "For You", Title: "For You",
				Intro: "The prepared pools personalised rows are drawn from.",
				Icon:  "m12 3 1.5 5 5 .2-4 3 1.4 5-3.9-2.8-3.9 2.8 1.4-5-4-3 5-.2L12 3Z",
			},
			{
				ID: "inspector", Label: "Score inspector", Title: "Score inspector",
				Intro: "Re-run the ranker for one person and read every component.",
				Icon:  "M10.5 17a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Zm4.6-1.9L20 20",
			},
		},
	},
	{
		Label: "Experience",
		Items: []adminNavItem{
			{
				ID: "features", Label: "Features", Title: "Features",
				Intro: "Roll out, stop and recover optional behaviour with no app release.",
				Icon:  "M4 7h10M18 7h2M4 17h2m4 0h10M14 4v6M6 14v6",
			},
			{
				ID: "playback", Label: "Playback", Title: "Playback",
				Intro: "Presentation policy sent with every playback launch.",
				Icon:  "M8 5v14l11-7zM4 5v14",
			},
			{
				ID: "subtitles", Label: "Subtitles", Title: "Subtitles",
				Intro: "Which providers a viewer may fetch a missing subtitle from.",
				Icon:  "M4 5.5h16v13H4zM7 15h5m3 0h2M7 11h3m2 0h5",
			},
			{
				ID: "updates", Label: "App updates", Title: "App updates",
				Intro: "Publish an optional or a required client update.",
				Icon:  "M12 16V4m0 0L8 8m4-4 4 4M5 13v6h14v-6",
			},
		},
	},
	{
		Label: "Operations",
		Items: []adminNavItem{
			{
				ID: "journeys", Label: "Journeys", Title: "User journeys",
				Intro: "How viewers move through Memby, use features and complete flows.",
				Icon:  "M4 6h5v5h6v7h5M7 3 4 6l3 3m10 6 3 3-3 3",
			},
			{
				ID: "maintenance", Label: "Maintenance", Title: "Maintenance",
				Intro: "Take Memby offline for every television.",
				Icon:  "m14.5 6.5 3-3 3 3-3 3M9 15l-5.5 5.5M13 4a5 5 0 0 0 6.5 6.5L10 20l-6-6 9.5-9.5Z",
			},
			{
				ID: "imports", Label: "Imports", Title: "Imports",
				Intro: "Catalogue synchronisation history.",
				Icon:  "M4 7h16v13H4zM8 4h8v3M8 12h8M8 16h5",
			},
			{
				ID: "engagement", Label: "Row engagement", Title: "Row engagement",
				Intro: "Impressions, focus, dwell and selections per launcher row.",
				Icon:  "M4 19V9m5 10V5m5 14v-7m5 7V3",
			},
			{
				ID: "searches", Label: "Searches", Title: "Searches",
				Intro: "What the household has been looking for, and what it searched just now.",
				Icon:  "M10.5 17a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Zm4.6-1.9L20 20",
			},
			{
				ID: "logs", Label: "Server logs", Title: "Server logs",
				Intro: "Structured gateway events as they happen.",
				Icon:  "M4 5h16v14H4zM7 9l2 2-2 2m5 1h5",
			},
		},
	},
}

// adminPages is the set of legal /admin/<page> URLs, derived so a page cannot exist in the
// rail and 404 — or be reachable and unnamed. installer_auth reads it to decide which
// destination a sign-in may return to, which is why hidden pages are excluded: they are
// addressed by a route that carries something else in the path, and a sign-in that returned
// to one without it would land on a page about nobody.
var adminPages = func() map[string]bool {
	pages := map[string]bool{}
	forEachAdminPage(func(item adminNavItem) {
		if !item.Hidden {
			pages[item.ID] = true
		}
	})
	return pages
}()

func forEachAdminPage(visit func(adminNavItem)) {
	for _, group := range adminNav {
		for _, item := range group.Items {
			visit(item)
		}
	}
}

type adminShellData struct {
	Page   adminNavItem
	Nav    []adminNavGroup
	CSS    template.CSS
	Core   template.JS
	Body   template.HTML
	Script template.JS
}

// adminRendered holds every page as finished bytes. Composition happens once, at start-up,
// so serving a page is a write of a []byte exactly as it was when the whole console was
// one file.
var adminRendered = buildAdminPages()

func buildAdminPages() map[string][]byte {
	shell := template.Must(template.New("shell").ParseFS(adminAssets, "admin/shell.html"))
	css := template.CSS(mustReadAdminAsset("admin/admin.css"))
	core := template.JS(mustReadAdminAsset("admin/core.js"))

	rendered := map[string][]byte{}
	forEachAdminPage(func(item adminNavItem) {
		var out bytes.Buffer
		err := shell.ExecuteTemplate(&out, "shell.html", adminShellData{
			Page: item, Nav: adminNav, CSS: css, Core: core,
			Body:   template.HTML(mustReadAdminAsset(path.Join("admin/pages", item.ID+".html"))),
			Script: template.JS(mustReadAdminAsset(path.Join("admin/pages", item.ID+".js"))),
		})
		if err != nil {
			panic(fmt.Sprintf("admin console: render %s: %v", item.ID, err))
		}
		rendered[item.ID] = out.Bytes()
	})
	assertEveryAdminFragmentIsRouted(rendered)
	return rendered
}

func mustReadAdminAsset(name string) string {
	data, err := adminAssets.ReadFile(name)
	if err != nil {
		panic(fmt.Sprintf("admin console: %v", err))
	}
	return string(data)
}

// A fragment nobody routes to is dead weight that still looks maintained. Catching it here
// means a page removed from adminNav takes its files with it, or fails at start-up.
func assertEveryAdminFragmentIsRouted(rendered map[string][]byte) {
	entries, err := fs.ReadDir(adminAssets, "admin/pages")
	if err != nil {
		panic(fmt.Sprintf("admin console: %v", err))
	}
	var orphaned []string
	for _, entry := range entries {
		name := entry.Name()
		if path.Ext(name) != ".html" {
			continue
		}
		id := name[:len(name)-len(".html")]
		if _, ok := rendered[id]; !ok {
			orphaned = append(orphaned, id)
		}
	}
	if len(orphaned) > 0 {
		sort.Strings(orphaned)
		panic(fmt.Sprintf("admin console: pages with no nav entry: %v", orphaned))
	}
}
