package controllers

import (
	"net/http"
	"encoding/json"
	"os"
	"path/filepath"
	"time"
	"fmt"
    "strings"
    "io"
    "image"
    _ "image/png"
    _ "image/jpeg"
    _ "image/gif"
    "net/http/httputil"

	"fotbal-club/internal/models"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

// ScoreboardController manages the singleton scoreboard state
type ScoreboardController struct {
	DB *gorm.DB
}

// --- Additional features ported from MyClub ScoreBoard ---

// makeShort derives a 3-letter uppercase abbreviation from a club name.
func makeShort(name string) string {
    name = strings.TrimSpace(name)
    if name == "" { return "---" }
    name = strings.ToUpper(name)
    repl := strings.NewReplacer(
        "Á", "A", "Ä", "A", "Å", "A", "Â", "A", "À", "A",
        "Č", "C", "Ć", "C", "Ç", "C",
        "Ď", "D",
        "É", "E", "Ě", "E", "È", "E", "Ë", "E", "Ê", "E",
        "Í", "I", "Ì", "I", "Ï", "I", "Î", "I",
        "Ň", "N", "Ń", "N",
        "Ó", "O", "Ö", "O", "Ô", "O", "Ò", "O",
        "Ř", "R",
        "Š", "S", "Ś", "S",
        "Ť", "T",
        "Ú", "U", "Ů", "U", "Ù", "U", "Ü", "U", "Û", "U",
        "Ý", "Y",
        "Ž", "Z",
    )
    name = repl.Replace(name)
    out := make([]rune, 0, 3)
    for _, r := range name {
        if r >= 'A' && r <= 'Z' {
            out = append(out, r)
            if len(out) == 3 { break }
        }
    }
    for len(out) < 3 { out = append(out, '-') }
    return string(out)
}

// DeriveColors returns average dominant colors from provided logo URLs
func (c *ScoreboardController) DeriveColors(ctx *gin.Context) {
    type req struct {
        URL string `json:"url"`
        HomeLogo string `json:"homeLogo"`
        AwayLogo string `json:"awayLogo"`
    }
    type singleResp struct{ Color string `json:"color"` }
    type duoResp struct{ PrimaryColor string `json:"primaryColor"`; SecondaryColor string `json:"secondaryColor"` }

    var q req
    q.URL = ctx.Query("url")
    q.HomeLogo = ctx.Query("homeLogo")
    q.AwayLogo = ctx.Query("awayLogo")
    if q.URL == "" && q.HomeLogo == "" && q.AwayLogo == "" {
        // try JSON body
        _ = ctx.ShouldBindJSON(&q)
    }

    if q.URL != "" {
        col, err := averageColorFromURL(q.URL)
        if err != nil {
            ctx.JSON(http.StatusBadRequest, gin.H{"error": "cannot fetch or process image"})
            return
        }
        ctx.JSON(http.StatusOK, singleResp{Color: col})
        return
    }
    if q.HomeLogo != "" || q.AwayLogo != "" {
        var primary, secondary string
        if q.HomeLogo != "" {
            if col, err := averageColorFromURL(q.HomeLogo); err == nil { primary = col }
        }
        if q.AwayLogo != "" {
            if col, err := averageColorFromURL(q.AwayLogo); err == nil { secondary = col }
        }
        ctx.JSON(http.StatusOK, duoResp{PrimaryColor: primary, SecondaryColor: secondary})
        return
    }
    ctx.JSON(http.StatusBadRequest, gin.H{"error": "provide ?url= or ?homeLogo=&awayLogo="})
}

// averageColorFromURL downloads an image and computes its average RGB color in hex.
func averageColorFromURL(u string) (string, error) {
    resp, err := http.Get(u)
    if err != nil { return "", err }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        // best-effort body capture for debugging
        b, _ := httputil.DumpResponse(resp, false)
        _ = b
        return "", fmt.Errorf("http status %d", resp.StatusCode)
    }
    img, _, err := image.Decode(resp.Body)
    if err != nil { return "", err }
    return averageHex(img), nil
}

func averageHex(img image.Image) string {
    rect := img.Bounds()
    if rect.Empty() { return "#000000" }
    w := rect.Dx(); h := rect.Dy()
    stepX, stepY := 1, 1
    for (w/stepX)*(h/stepY) > 160000 {
        if stepX <= stepY { stepX *= 2 } else { stepY *= 2 }
    }
    var rsum, gsum, bsum, count uint64
    for y := rect.Min.Y; y < rect.Max.Y; y += stepY {
        for x := rect.Min.X; x < rect.Max.X; x += stepX {
            cr, cg, cb, ca := img.At(x,y).RGBA()
            if ca < 0x2000 { continue }
            rsum += uint64(cr >> 8)
            gsum += uint64(cg >> 8)
            bsum += uint64(cb >> 8)
            count++
        }
    }
    if count == 0 { return "#000000" }
    r8 := uint8(rsum / count)
    g8 := uint8(gsum / count)
    b8 := uint8(bsum / count)
    return fmt.Sprintf("#%02x%02x%02x", r8, g8, b8)
}

// SwapSides swaps home and away team info including names, logos, shorts, scores and colors.
func (c *ScoreboardController) SwapSides(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    s.HomeName, s.AwayName = s.AwayName, s.HomeName
    s.HomeLogoURL, s.AwayLogoURL = s.AwayLogoURL, s.HomeLogoURL
    s.HomeScore, s.AwayScore = s.AwayScore, s.HomeScore
    s.HomeShort, s.AwayShort = s.AwayShort, s.HomeShort
    s.PrimaryColor, s.SecondaryColor = s.SecondaryColor, s.PrimaryColor
    if err := c.DB.Save(s).Error; err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
    }
    ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

// StartSecondHalf swaps sides, resets the timer to 00:00 and immediately starts it.
func (c *ScoreboardController) StartSecondHalf(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    // swap first
    s.HomeName, s.AwayName = s.AwayName, s.HomeName
    s.HomeLogoURL, s.AwayLogoURL = s.AwayLogoURL, s.HomeLogoURL
    s.HomeScore, s.AwayScore = s.AwayScore, s.HomeScore
    s.HomeShort, s.AwayShort = s.AwayShort, s.HomeShort
    s.PrimaryColor, s.SecondaryColor = s.SecondaryColor, s.PrimaryColor
    // reset and start timer for next half
    s.Running = true
    s.ElapsedSeconds = 0
    s.TimerStartUnix = time.Now().Unix()
    s.Timer = "00:00"
    if err := c.DB.Save(s).Error; err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
    }
    ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

// SaveState saves current scoreboard state as a JSON file in /saved directory.
func (c *ScoreboardController) SaveState(ctx *gin.Context) {
    type req struct{ Filename string `json:"filename"` }
    var q req
    _ = ctx.ShouldBindJSON(&q)
    if q.Filename == "" { q.Filename = ctx.Query("filename") }
    name := sanitizeFilename(q.Filename)
    if name == "" { name = time.Now().Format("20060102-150405") }
    if !strings.HasSuffix(strings.ToLower(name), ".json") { name += ".json" }
    _ = os.MkdirAll("saved", 0o755)
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    b, _ := json.MarshalIndent(s, "", "  ")
    if err := os.WriteFile(filepath.Join("saved", name), b, 0o644); err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "save failed"}); return
    }
    ctx.JSON(http.StatusOK, gin.H{"saved": name})
}

// ListSaves returns the list of saved preset filenames from /saved
func (c *ScoreboardController) ListSaves(ctx *gin.Context) {
    entries, err := os.ReadDir("saved")
    if err != nil { ctx.JSON(http.StatusOK, []string{}) ; return }
    out := make([]string, 0, len(entries))
    for _, e := range entries {
        if e.IsDir() { continue }
        name := e.Name()
        if strings.HasSuffix(strings.ToLower(name), ".json") { out = append(out, name) }
    }
    ctx.JSON(http.StatusOK, out)
}

// LoadSaved loads a saved preset from /saved/<filename> and applies it to the singleton.
func (c *ScoreboardController) LoadSaved(ctx *gin.Context) {
    // Support filename via query, JSON, or multipart form file upload as raw JSON
    filename := sanitizeFilename(ctx.Query("filename"))
    var body struct{ Filename string `json:"filename"` }
    if filename == "" {
        _ = ctx.ShouldBindJSON(&body)
        filename = sanitizeFilename(body.Filename)
    }
    if filename == "" {
        // try multipart file upload under field "file"
        file, _, err := ctx.Request.FormFile("file")
        if err == nil {
            defer file.Close()
            data, err := io.ReadAll(file)
            if err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "cannot read file"}); return }
            var imported models.ScoreboardState
            if err := json.Unmarshal(data, &imported); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}); return }
            applyImportedState(imported, c, ctx)
            return
        }
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "missing filename"})
        return
    }
    if !strings.HasSuffix(strings.ToLower(filename), ".json") { filename += ".json" }
    path := filepath.Join("saved", filename)
    data, err := os.ReadFile(path)
    if err != nil { ctx.JSON(http.StatusNotFound, gin.H{"error": "file not found"}); return }
    var imported models.ScoreboardState
    if err := json.Unmarshal(data, &imported); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}); return }
    applyImportedState(imported, c, ctx)
}

func applyImportedState(imported models.ScoreboardState, c *ScoreboardController, ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    // overwrite relevant fields
    s.HomeName = imported.HomeName
    s.AwayName = imported.AwayName
    s.HomeLogoURL = imported.HomeLogoURL
    s.AwayLogoURL = imported.AwayLogoURL
    // derive shorts if empty
    if strings.TrimSpace(imported.HomeShort) != "" { s.HomeShort = imported.HomeShort } else { s.HomeShort = makeShort(s.HomeName) }
    if strings.TrimSpace(imported.AwayShort) != "" { s.AwayShort = imported.AwayShort } else { s.AwayShort = makeShort(s.AwayName) }
    if imported.PrimaryColor != "" { s.PrimaryColor = imported.PrimaryColor }
    if imported.SecondaryColor != "" { s.SecondaryColor = imported.SecondaryColor }
    s.HomeScore = imported.HomeScore
    s.AwayScore = imported.AwayScore
    if imported.HalfLength > 0 { s.HalfLength = imported.HalfLength }
    if imported.Theme != "" { s.Theme = imported.Theme }
    // timer handling
    base := parseTimerToSeconds(imported.Timer)
    s.Timer = fmt.Sprintf("%02d:%02d", base/60, base%60)
    s.ElapsedSeconds = base
    if imported.Running {
        s.Running = true
        s.TimerStartUnix = time.Now().Unix() - int64(base)
    } else {
        s.Running = false
        s.TimerStartUnix = 0
    }
    if err := c.DB.Save(s).Error; err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return }
    ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

// sanitizeFilename keeps only [a-zA-Z0-9-_] and dots, strips path separators
func sanitizeFilename(in string) string {
    in = strings.TrimSpace(in)
    in = strings.ReplaceAll(in, "\\", "")
    in = strings.ReplaceAll(in, "/", "")
    var b strings.Builder
    for _, r := range in {
        if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' || r == '.' {
            b.WriteRune(r)
        }
    }
    return b.String()
}

// --- Timer helpers and handlers ---

func parseTimerToSeconds(timer string) int {
    // Expect MM:SS
    if len(timer) < 4 {
        return 0
    }
    var m, s int
    _, err := fmt.Sscanf(timer, "%d:%d", &m, &s)
    if err != nil || m < 0 || s < 0 || s >= 60 {
        return 0
    }
    return m*60 + s
}

func formatSeconds(sec int) string {
    if sec < 0 { sec = 0 }
    return fmt.Sprintf("%02d:%02d", sec/60, sec%60)
}

func computeTimer(s models.ScoreboardState) (timer string, running bool) {
    running = s.Running
    base := s.ElapsedSeconds
    if s.Running {
        now := time.Now().Unix()
        if s.TimerStartUnix > 0 {
            diff := int(now - s.TimerStartUnix)
            if diff > 0 { base = diff } else { base = 0 }
        }
    }
    // Cap by half length
    cap := s.HalfLength * 60
    if cap <= 0 { cap = 45 * 60 }
    if base >= cap {
        base = cap
        running = false
    }
    timer = formatSeconds(base)
    return
}

// StartTimer sets running=true and backdates TimerStartUnix
func (c *ScoreboardController) StartTimer(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    if s.ElapsedSeconds == 0 && s.Timer != "" {
        s.ElapsedSeconds = parseTimerToSeconds(s.Timer)
    }
    s.TimerStartUnix = time.Now().Unix() - int64(s.ElapsedSeconds)
    s.Running = true
    if err := c.DB.Save(s).Error; err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
    }
    ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

// PauseTimer sets running=false and fixes elapsedSeconds
func (c *ScoreboardController) PauseTimer(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    if s.Running {
        now := time.Now().Unix()
        if s.TimerStartUnix > 0 {
            diff := int(now - s.TimerStartUnix)
            if diff > 0 { s.ElapsedSeconds = diff } else { s.ElapsedSeconds = 0 }
        }
    }
    s.Running = false
    // Cap and set display string
    cap := s.HalfLength * 60
    if cap <= 0 { cap = 45 * 60 }
    if s.ElapsedSeconds > cap { s.ElapsedSeconds = cap }
    s.Timer = formatSeconds(s.ElapsedSeconds)
    if err := c.DB.Save(s).Error; err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
    }
    ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

// ResetTimer clears timer to 00:00 and stops it
func (c *ScoreboardController) ResetTimer(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"}); return }
    s.Running = false
    s.ElapsedSeconds = 0
    s.TimerStartUnix = 0
    s.Timer = "00:00"
    if err := c.DB.Save(s).Error; err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"}); return
    }
    ctx.JSON(http.StatusOK, gin.H{"ok": true})
}

func NewScoreboardController(db *gorm.DB) *ScoreboardController {
	return &ScoreboardController{DB: db}
}

func (c *ScoreboardController) getOrCreateSingleton() (*models.ScoreboardState, error) {
    var s models.ScoreboardState
    if err := c.DB.First(&s).Error; err != nil {
        if err == gorm.ErrRecordNotFound {
            // Create with sensible defaults
            s = models.ScoreboardState{
                HomeName:      "DOMÁCÍ",
                AwayName:      "HOSTÉ",
                HomeShort:     "DOM",
                AwayShort:     "HOS",
                PrimaryColor:  "#1e3a8a",
                SecondaryColor: "#2563eb",
                HalfLength:    45,
                Theme:         "pill",
                Timer:         "00:00",
                Running:       false,
                TimerStartUnix: 0,
                ElapsedSeconds: 0,
                // New fields defaults
                SidesFlipped: false,
                Half:         1,
                QRShowEveryMinutes:    5,
                QRShowDurationSeconds: 60,
            }
            if err := c.DB.Create(&s).Error; err != nil {
                return nil, err
            }
            return &s, nil
        }
        return nil, err
    }
    // Ensure defaults for newly added fields when loading existing row
    changed := false
    if s.Half == 0 { s.Half = 1; changed = true }
    if s.QRShowEveryMinutes == 0 { s.QRShowEveryMinutes = 5; changed = true }
    if s.QRShowDurationSeconds == 0 { s.QRShowDurationSeconds = 60; changed = true }
    if changed { _ = c.DB.Save(&s).Error }
    return &s, nil
}

// GetPublic returns read-only scoreboard state for public overlay
func (c *ScoreboardController) GetPublic(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"})
        return
    }
    timer, running := computeTimer(*s)
    ctx.JSON(http.StatusOK, gin.H{
        "homeName":       s.HomeName,
        "awayName":       s.AwayName,
        "homeLogo":       s.HomeLogoURL,
        "awayLogo":       s.AwayLogoURL,
        "homeShort":      s.HomeShort,
        "awayShort":      s.AwayShort,
        "primaryColor":   s.PrimaryColor,
        "secondaryColor": s.SecondaryColor,
        "homeScore":      s.HomeScore,
        "awayScore":      s.AwayScore,
        "halfLength":     s.HalfLength,
        "theme":          s.Theme,
        "external_match_id": s.ExternalMatchID,
        "active":            s.Active,
        // Newly exposed fields compatible with MyClub ScoreBoard overlay
        "sidesFlipped": s.SidesFlipped,
        "half":         s.Half,
        "qrEvery":      s.QRShowEveryMinutes,
        "qrDuration":   s.QRShowDurationSeconds,
        "timer":             timer,
        "running":           running,
        "timer_start_unix":  s.TimerStartUnix,
        "elapsed_seconds":   s.ElapsedSeconds,
    })
}

// GetAdmin returns full state for admins
func (c *ScoreboardController) GetAdmin(ctx *gin.Context) {
    s, err := c.getOrCreateSingleton()
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"})
        return
    }
    // Compute transient timer display
    timer, running := computeTimer(*s)
    out := *s
    out.Timer = timer
    out.Running = running
    ctx.JSON(http.StatusOK, out)
}

// PutAdmin updates the singleton state (admin only)
func (c *ScoreboardController) PutAdmin(ctx *gin.Context) {
    var payload struct {
        HomeName       *string `json:"homeName"`
        AwayName       *string `json:"awayName"`
        HomeLogo       *string `json:"homeLogo"`
        AwayLogo       *string `json:"awayLogo"`
        HomeShort      *string `json:"homeShort"`
        AwayShort      *string `json:"awayShort"`
        PrimaryColor   *string `json:"primaryColor"`
        SecondaryColor *string `json:"secondaryColor"`
        HomeScore      *int    `json:"homeScore"`
        AwayScore      *int    `json:"awayScore"`
        HalfLength     *int    `json:"halfLength"`
        Theme          *string `json:"theme"`
        ExternalMatchID *string `json:"externalMatchId"`
        Active          *bool   `json:"active"`
        // Optional direct timer patch (when not running)
        Timer           *string `json:"timer"`
        // Newly supported fields (ported from MyClub ScoreBoard)
        SidesFlipped   *bool   `json:"sidesFlipped"`
        Half           *int    `json:"half"`
        QRShowEveryMinutes    *int `json:"qrEvery"`
        QRShowDurationSeconds *int `json:"qrDuration"`
    }
    if err := ctx.ShouldBindJSON(&payload); err != nil {
        ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
        return
    }
    s, err := c.getOrCreateSingleton()
    if err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "cannot load scoreboard"})
        return
    }
    // Apply patch
    if payload.HomeName != nil { s.HomeName = *payload.HomeName }
    if payload.AwayName != nil { s.AwayName = *payload.AwayName }
    if payload.HomeLogo != nil { s.HomeLogoURL = *payload.HomeLogo }
    if payload.AwayLogo != nil { s.AwayLogoURL = *payload.AwayLogo }
    if payload.HomeShort != nil { s.HomeShort = *payload.HomeShort }
    if payload.AwayShort != nil { s.AwayShort = *payload.AwayShort }
    if payload.PrimaryColor != nil { s.PrimaryColor = *payload.PrimaryColor }
    if payload.SecondaryColor != nil { s.SecondaryColor = *payload.SecondaryColor }
    if payload.HomeScore != nil { s.HomeScore = *payload.HomeScore }
    if payload.AwayScore != nil { s.AwayScore = *payload.AwayScore }
    if payload.HalfLength != nil { s.HalfLength = *payload.HalfLength }
    if payload.Theme != nil { s.Theme = *payload.Theme }
    if payload.ExternalMatchID != nil { s.ExternalMatchID = *payload.ExternalMatchID }
    if payload.Active != nil { s.Active = *payload.Active }
    if payload.SidesFlipped != nil { s.SidesFlipped = *payload.SidesFlipped }
    if payload.Half != nil { s.Half = *payload.Half }
    if payload.QRShowEveryMinutes != nil && *payload.QRShowEveryMinutes > 0 { s.QRShowEveryMinutes = *payload.QRShowEveryMinutes }
    if payload.QRShowDurationSeconds != nil && *payload.QRShowDurationSeconds > 0 { s.QRShowDurationSeconds = *payload.QRShowDurationSeconds }
    if payload.Timer != nil && !s.Running {
        // Set base timer string when paused
        s.Timer = *payload.Timer
        s.ElapsedSeconds = parseTimerToSeconds(*payload.Timer)
    }

    if err := c.DB.Save(s).Error; err != nil {
        ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save"})
        return
    }

	// Best-effort: if scoreboard active and linked to a match, write live cache
	if s.Active && s.ExternalMatchID != "" {
		go writeLiveScoreboardCache(s)
	}
    ctx.JSON(http.StatusOK, s)
}

// writeLiveScoreboardCache writes cache/live/score_<id>.json and patches cache/prefetch/events_upcoming.json if present
func writeLiveScoreboardCache(s *models.ScoreboardState) {
	// Ensure directories
	_ = os.MkdirAll(filepath.Join("cache", "live"), 0o755)
	// Write live file
	payload := map[string]any{
		"external_match_id": s.ExternalMatchID,
		"home": s.HomeName,
		"away": s.AwayName,
		"home_logo_url": s.HomeLogoURL,
		"away_logo_url": s.AwayLogoURL,
		"home_score": s.HomeScore,
		"away_score": s.AwayScore,
		"primary_color": s.PrimaryColor,
		"secondary_color": s.SecondaryColor,
		"theme": s.Theme,
		"half_length": s.HalfLength,
		"active": s.Active,
		"updated_at": time.Now().Format(time.RFC3339),
	}
	b, _ := json.MarshalIndent(payload, "", "  ")
	tmp := filepath.Join("cache", "live", "score_"+s.ExternalMatchID+".json.tmp")
	dst := filepath.Join("cache", "live", "score_"+s.ExternalMatchID+".json")
	if err := os.WriteFile(tmp, b, 0o644); err == nil { _ = os.Rename(tmp, dst) }

	// Patch prefetch events if available
	prefetch := filepath.Join("cache", "prefetch", "events_upcoming.json")
	f, err := os.Open(prefetch)
	if err != nil { return }
	defer f.Close()
	var arr []map[string]any
	if err := json.NewDecoder(f).Decode(&arr); err != nil { return }
	for i := range arr {
		id := ""
		if v, ok := arr[i]["match_id"].(string); ok { id = v }
		if id == s.ExternalMatchID {
			arr[i]["score"] = map[string]any{"home": s.HomeScore, "away": s.AwayScore}
			arr[i]["home_logo_url"] = s.HomeLogoURL
			arr[i]["away_logo_url"] = s.AwayLogoURL
			arr[i]["home"] = s.HomeName
			arr[i]["away"] = s.AwayName
			break
		}
	}
	out, _ := json.MarshalIndent(arr, "", "  ")
	_ = os.WriteFile(prefetch+".tmp", out, 0o644)
	_ = os.Rename(prefetch+".tmp", prefetch)
}
