package main

// Web publishing — the Wails boundary for internal/webpub, plus the scheduling.
//
// Two triggers, deliberately: a QSO is logged, or the periodic timer fires.
// Both go through publishSoon, which DEBOUNCES: a run of contacts must not
// produce one FTP session per QSO, and a page that is fifteen seconds stale is
// indistinguishable from a live one to anybody reading it on the web.

import (
	"encoding/json"
	"fmt"
	"strings"
	"sync"
	"time"

	"hamlog/internal/applog"
	"hamlog/internal/qso"
	"hamlog/internal/webpub"

	wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)

// publishDebounce is how long a publish request waits for company. Long enough
// to fold a burst of logging into one upload, short enough that the page looks
// live to a reader who just heard you on the air.
const publishDebounce = 15 * time.Second

type webPublisher struct {
	mu      sync.Mutex
	timer   *time.Timer
	ticker  *time.Ticker
	tickStp chan struct{}
	last    time.Time
	lastErr string
}

// WebPublishStatus is what the settings panel shows under the buttons.
//
// No column list here: the panel gets that from WebPublishColumns(). An
// anonymous struct in a bound type also breaks the Wails generator, which has
// no name to emit for it.
type WebPublishStatus struct {
	LastRun string `json:"last_run"` // "" = never this session
	LastErr string `json:"last_err"`
}

// GetWebPublishConfig reads the stored configuration (defaults applied).
func (a *App) GetWebPublishConfig() (webpub.Config, error) {
	var cfg webpub.Config
	if a.settings == nil {
		cfg.Normalise()
		return cfg, fmt.Errorf("db not initialized")
	}
	raw, err := a.settings.Get(a.ctx, keyWebPublish)
	if err == nil && strings.TrimSpace(raw) != "" {
		// A locked vault hands back "" rather than ciphertext — that reads as "not
		// configured", which is exactly right here: publishing must not run with a
		// password we cannot decrypt.
		_ = json.Unmarshal([]byte(raw), &cfg)
	}
	cfg.Normalise()
	return cfg, nil
}

// SaveWebPublishConfig persists it and restarts the periodic timer.
func (a *App) SaveWebPublishConfig(cfg webpub.Config) error {
	if a.settings == nil {
		return fmt.Errorf("db not initialized")
	}
	cfg.Normalise()
	b, err := json.Marshal(cfg)
	if err != nil {
		return err
	}
	if err := a.settings.Set(a.ctx, keyWebPublish, string(b)); err != nil {
		return err
	}
	a.restartWebPublishTimer()
	return nil
}

// WebPublishColumns lists the offered columns for the picker.
func (a *App) WebPublishColumns() []map[string]string {
	cols := webpub.KnownColumnKeys()
	out := make([]map[string]string, 0, len(cols))
	for _, c := range cols {
		out = append(out, map[string]string{"key": c.Key, "header": c.Header})
	}
	return out
}

// TestWebPublishFTP validates the server settings without uploading anything.
func (a *App) TestWebPublishFTP(cfg webpub.Config) (string, error) {
	return webpub.Test(cfg)
}

// PublishLogNow renders and publishes immediately, ignoring the debounce, and
// reports what happened. This is the "Publish now" button: the operator is
// waiting on the answer, so it runs synchronously and returns the real error.
func (a *App) PublishLogNow() (string, error) {
	cfg, _ := a.GetWebPublishConfig()
	return a.publish(cfg)
}

// publish does the work: read the QSOs, render, write locally, then upload.
//
// Local FIRST and upload second, always. A network failure then leaves a good
// file on disk that the operator can publish another way, instead of a
// truncated one on the server.
func (a *App) publish(cfg webpub.Config) (string, error) {
	if a.qso == nil {
		return "", fmt.Errorf("logbook not ready")
	}
	cfg.Normalise()
	qsos, err := a.qso.List(a.ctx, qso.ListFilter{Limit: cfg.Count})
	if err != nil {
		return "", fmt.Errorf("read the log: %w", err)
	}
	station := ""
	if p, perr := a.profiles.Active(a.ctx); perr == nil {
		station = p.Callsign
	}
	data, err := webpub.Render(cfg, qsos, station)
	if err != nil {
		return "", fmt.Errorf("build the page: %w", err)
	}
	path, err := webpub.WriteLocal(cfg, data)
	if err != nil {
		return "", err
	}
	msg := fmt.Sprintf("%d QSO → %s", len(qsos), path)
	if cfg.FTPEnabled {
		if err := webpub.Upload(cfg, data); err != nil {
			// The local file IS written — say so, so the operator knows the failure
			// is the transfer and not the export.
			return msg, fmt.Errorf("written locally, but the upload failed: %w", err)
		}
		msg += fmt.Sprintf(" → ftp://%s/%s", cfg.FTPHost, strings.TrimPrefix(cfg.FTPFolder+"/"+cfg.FTPFileName, "/"))
	}
	return msg, nil
}

// publishSoon schedules a debounced publish. Called on every logged QSO.
func (a *App) publishSoon() {
	cfg, _ := a.GetWebPublishConfig()
	if !cfg.Enabled {
		return
	}
	a.webpub.mu.Lock()
	defer a.webpub.mu.Unlock()
	if a.webpub.timer != nil {
		a.webpub.timer.Stop()
	}
	a.webpub.timer = time.AfterFunc(publishDebounce, a.publishNowBackground)
}

// publishNowBackground runs a scheduled publish and records the outcome for the
// settings panel. Never surfaces a dialog: this fires while the operator is
// working, and a web server that is down must not interrupt logging.
func (a *App) publishNowBackground() {
	cfg, _ := a.GetWebPublishConfig()
	if !cfg.Enabled {
		return
	}
	msg, err := a.publish(cfg)
	a.webpub.mu.Lock()
	a.webpub.last = time.Now()
	if err != nil {
		a.webpub.lastErr = err.Error()
	} else {
		a.webpub.lastErr = ""
	}
	a.webpub.mu.Unlock()
	if err != nil {
		applog.Printf("webpublish: %v", err)
	} else {
		applog.Printf("webpublish: %s", msg)
	}
	if a.ctx != nil {
		wruntime.EventsEmit(a.ctx, "webpublish:done", msg)
	}
}

// GetWebPublishStatus reports the last run for the settings panel.
func (a *App) GetWebPublishStatus() WebPublishStatus {
	var st WebPublishStatus
	a.webpub.mu.Lock()
	if !a.webpub.last.IsZero() {
		st.LastRun = a.webpub.last.UTC().Format("2006-01-02 15:04:05") + " UTC"
	}
	st.LastErr = a.webpub.lastErr
	a.webpub.mu.Unlock()
	return st
}

// restartWebPublishTimer (re)arms the periodic refresh from the saved interval.
// Stopped and rebuilt on every save, so a changed interval takes effect at once
// rather than after the old one has fired.
func (a *App) restartWebPublishTimer() {
	a.webpub.mu.Lock()
	if a.webpub.tickStp != nil {
		close(a.webpub.tickStp)
		a.webpub.tickStp = nil
	}
	a.webpub.mu.Unlock()

	cfg, _ := a.GetWebPublishConfig()
	if !cfg.Enabled || cfg.IntervalMin <= 0 {
		return
	}
	stop := make(chan struct{})
	a.webpub.mu.Lock()
	a.webpub.tickStp = stop
	a.webpub.mu.Unlock()

	go func(every time.Duration) {
		t := time.NewTicker(every)
		defer t.Stop()
		for {
			select {
			case <-stop:
				return
			case <-t.C:
				a.publishNowBackground()
			}
		}
	}(time.Duration(cfg.IntervalMin) * time.Minute)
}
