package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
	_ "time/tzdata" // embed the IANA tz database so America/Los_Angeles resolves
	// even in debian:12-slim, which ships no /usr/share/zoneinfo.
)

// resolveRegionDir finds the active world's Region dir under a 7DTD saves root
// (the dir mounted as the saves volume — holds serverconfig.xml + .local/...).
// Prefers serverconfig GameWorld/GameName when that world has real data
// (main.ttw); else falls back to the most-recently-written Saves/*/<GameName>
// with data — handles RWG seed-named worlds.
func resolveRegionDir(savesRoot string) (string, error) {
	raw, err := os.ReadFile(filepath.Join(savesRoot, "serverconfig.xml"))
	if err != nil {
		return "", fmt.Errorf("read serverconfig.xml: %w", err)
	}
	gw := xmlProp(string(raw), "GameWorld")
	gn := xmlProp(string(raw), "GameName")
	base := filepath.Join(savesRoot, ".local", "share", "7DaysToDie", "Saves")
	hasWorld := func(d string) bool {
		if _, e := os.Stat(filepath.Join(d, "main.ttw")); e == nil {
			return true
		}
		fi, e := os.Stat(filepath.Join(d, "Region"))
		return e == nil && fi.IsDir()
	}
	if gw != "" && gn != "" {
		lit := filepath.Join(base, gw, gn)
		if hasWorld(lit) {
			return filepath.Join(lit, "Region"), nil
		}
	}
	if gn != "" { // RWG fallback: newest world dir with this GameName.
		worlds, _ := os.ReadDir(base)
		var newest string
		var newestT time.Time
		for _, w := range worlds {
			if !w.IsDir() {
				continue
			}
			cand := filepath.Join(base, w.Name(), gn)
			if !hasWorld(cand) {
				continue
			}
			if fi, e := os.Stat(cand); e == nil && fi.ModTime().After(newestT) {
				newestT, newest = fi.ModTime(), cand
			}
		}
		if newest != "" {
			return filepath.Join(newest, "Region"), nil
		}
	}
	return "", fmt.Errorf("could not resolve active Region dir (GameWorld=%q GameName=%q)", gw, gn)
}

// xmlProp pulls the value from <property name="X" value="Y" /> (tolerant of
// attribute order + quote style; skips comments).
func xmlProp(content, name string) string {
	for _, raw := range strings.Split(content, "\n") {
		line := strings.TrimSpace(raw)
		if line == "" || strings.HasPrefix(line, "<!--") {
			continue
		}
		if !strings.Contains(line, `name="`+name+`"`) {
			continue
		}
		i := strings.Index(line, "value=")
		if i < 0 {
			continue
		}
		rest := line[i+len("value="):]
		if len(rest) < 1 {
			continue
		}
		q := rest[0]
		rest = rest[1:]
		if end := strings.IndexByte(rest, q); end >= 0 {
			return strings.TrimSpace(rest[:end])
		}
	}
	return ""
}

// postDiscordHealReport posts a rich embed heal summary to a Discord channel via
// a bot token read from tokenFile. No-op when token/channel missing or nothing
// healed. All times are rendered in Pacific (America/Los_Angeles → PST/PDT).
func postDiscordHealReport(tokenFile, channel, label string, rep ahReport) error {
	if tokenFile == "" || channel == "" || len(rep.Healed) == 0 {
		return nil
	}
	tb, err := os.ReadFile(tokenFile)
	if err != nil {
		return fmt.Errorf("read discord token: %w", err)
	}
	token := strings.TrimSpace(string(tb))
	if token == "" {
		return nil
	}
	if label == "" {
		label = "7DTD server"
	}
	loc, lerr := time.LoadLocation("America/Los_Angeles")
	if lerr != nil || loc == nil {
		loc = time.UTC
	}
	pst := func(rfc string) string {
		if t, e := time.Parse(time.RFC3339, rfc); e == nil {
			return t.In(loc).Format("Jan 2, 3:04 PM MST")
		}
		return rfc
	}
	srcLabel := func(s string) string {
		switch {
		case strings.HasPrefix(s, "rolling:"):
			return "rolling snapshot"
		case strings.HasPrefix(s, "backup:"):
			return "panel backup"
		default:
			return s
		}
	}
	var lines []string
	for _, h := range rep.Healed {
		lines = append(lines, fmt.Sprintf("• `%s` ← %s _(saved %s)_", h.Region, srcLabel(h.Source), pst(h.SourceTime)))
	}
	color := 0x57F287 // green — everything healed
	fields := []map[string]any{{
		"name":   fmt.Sprintf("✅ Healed %d region(s)", len(rep.Healed)),
		"value":  strings.Join(lines, "\n"),
		"inline": false,
	}}
	if len(rep.Unhealable) > 0 {
		color = 0xED4245 // red — some couldn't be healed
		fields = append(fields, map[string]any{
			"name":   fmt.Sprintf("⚠️ Could NOT heal %d (no clean copy anywhere)", len(rep.Unhealable)),
			"value":  "`" + strings.Join(rep.Unhealable, "`, `") + "`",
			"inline": false,
		})
	}
	nowPST := time.Now().In(loc).Format("Mon Jan 2, 3:04 PM MST")
	embed := map[string]any{
		"title":       "🩺 Region Medic — " + label,
		"description": fmt.Sprintf("Detected and repaired **%d** corrupt region file(s) on reboot, before the world loaded.", rep.CorruptFound),
		"color":       color,
		"fields":      fields,
		"footer":      map[string]any{"text": "Region Medic · " + nowPST},
	}
	body, _ := json.Marshal(map[string]any{"embeds": []any{embed}})
	req, err := http.NewRequest(http.MethodPost, "https://discord.com/api/v10/channels/"+channel+"/messages", bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bot "+token)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "RefugeBot-RegionMedic (https://refugegaming.org, 1.0)")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode/100 != 2 {
		b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
		return fmt.Errorf("discord HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
	}
	return nil
}

// savesMedicConfig is the per-server medic config the panel writes (via FsWrite,
// no container recreate) into the saves volume at region-medic.json. Enabled is a
// pointer so an absent field defaults to ON.
type savesMedicConfig struct {
	Enabled        *bool  `json:"enabled"`
	Keep           int    `json:"keep"`
	World          string `json:"world"` // "<World>/<GameName>"; empty = auto-detect active world
	DiscordChannel string `json:"discord_channel"`
}

// readSavesMedicConfig reads <savesRoot>/region-medic.json; ok=false if absent or
// unparseable (the caller then keeps its flag defaults).
func readSavesMedicConfig(savesRoot string) (savesMedicConfig, bool) {
	b, err := os.ReadFile(filepath.Join(savesRoot, "region-medic.json"))
	if err != nil {
		return savesMedicConfig{}, false
	}
	var c savesMedicConfig
	if json.Unmarshal(b, &c) != nil {
		return savesMedicConfig{}, false
	}
	return c, true
}
