package server

import (
	"crypto/subtle"
	"database/sql"
	"encoding/json"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/operator/command-center/internal/notifications"
)

// portSyncReq is the envelope the local Windows port-sync script POSTs when the
// Proton/qBittorrent ports drift (and get corrected) or a sync fails. See
// auto-port-forward/ProtonPortUpdate.ps1.
type portSyncReq struct {
	Title    string         `json:"title"`
	Body     string         `json:"body"`
	Severity string         `json:"severity"`
	Data     map[string]any `json:"data"`
}

// handlePortSyncReport ingests a port-sync event from the home PC and fans it
// out as a Web Push. Like /api/disk/report it is WebAuthn-exempt (the script
// can't do a passkey ceremony) and authenticates with a bearer token from
// CC_PORTSYNC_TOKEN instead.
func (s *Server) handlePortSyncReport(w http.ResponseWriter, r *http.Request) {
	if s.portSyncToken == "" {
		http.Error(w, "port-sync reporting not configured (no CC_PORTSYNC_TOKEN)", http.StatusServiceUnavailable)
		return
	}
	got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
	if subtle.ConstantTimeCompare([]byte(got), []byte(s.portSyncToken)) != 1 {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req portSyncReq
	if err := json.NewDecoder(io.LimitReader(r.Body, 8192)).Decode(&req); err != nil {
		http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
		return
	}
	if strings.TrimSpace(req.Title) == "" {
		http.Error(w, "title required", http.StatusBadRequest)
		return
	}

	sev := notifications.SeverityInfo
	switch strings.ToLower(req.Severity) {
	case "critical":
		sev = notifications.SeverityCritical
	case "warning":
		sev = notifications.SeverityWarning
	}

	if s.notifications == nil || s.notifications.Dispatcher == nil {
		// Accept the report but note push isn't wired — keeps the script happy.
		writeJSON(w, http.StatusAccepted, map[string]any{"ok": true, "pushed": false})
		return
	}

	s.notifications.Dispatcher.Dispatch(r.Context(), notifications.Notification{
		RuleName: "Proton port sync",
		Title:    req.Title,
		Body:     req.Body,
		Severity: sev,
		URL:      "/torrents",
		Data:     req.Data,
	}, []string{"push"}, nil)
	s.logger.Info().Str("severity", string(sev)).Str("title", req.Title).Msg("port-sync event pushed")

	writeJSON(w, http.StatusAccepted, map[string]any{"ok": true, "pushed": true})
}

// portSyncStatusReq is the per-run status heartbeat (no push). The home-PC
// script POSTs it every cycle so the dashboard can show live port state.
type portSyncStatusReq struct {
	ProtonPort  int    `json:"proton_port"`
	QbitPort    int    `json:"qbit_port"`
	InSync      bool   `json:"in_sync"`
	VpnUp       bool   `json:"vpn_up"`
	QbitRunning bool   `json:"qbit_running"`
	Note        string `json:"note"`
}

type portSyncStatusResp struct {
	ProtonPort  int    `json:"proton_port"`
	QbitPort    int    `json:"qbit_port"`
	InSync      bool   `json:"in_sync"`
	VpnUp       bool   `json:"vpn_up"`
	QbitRunning bool   `json:"qbit_running"`
	Note        string `json:"note"`
	ReportedAt  int64  `json:"reported_at"`
}

// handlePortSyncStatusReport upserts the single latest-status row. Token-authed
// + WebAuthn-exempt like the other machine ingress endpoints.
func (s *Server) handlePortSyncStatusReport(w http.ResponseWriter, r *http.Request) {
	if s.sqlite == nil {
		http.Error(w, "unavailable", http.StatusServiceUnavailable)
		return
	}
	if s.portSyncToken == "" {
		http.Error(w, "port-sync reporting not configured (no CC_PORTSYNC_TOKEN)", http.StatusServiceUnavailable)
		return
	}
	got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
	if subtle.ConstantTimeCompare([]byte(got), []byte(s.portSyncToken)) != 1 {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}

	var req portSyncStatusReq
	if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&req); err != nil {
		http.Error(w, "bad json: "+err.Error(), http.StatusBadRequest)
		return
	}

	if _, err := s.sqlite.ExecContext(r.Context(), `
		INSERT INTO port_sync_status (id, proton_port, qbit_port, in_sync, vpn_up, qbit_running, note, reported_at)
		VALUES (1, ?, ?, ?, ?, ?, ?, ?)
		ON CONFLICT(id) DO UPDATE SET
			proton_port=excluded.proton_port, qbit_port=excluded.qbit_port,
			in_sync=excluded.in_sync, vpn_up=excluded.vpn_up,
			qbit_running=excluded.qbit_running, note=excluded.note,
			reported_at=excluded.reported_at`,
		req.ProtonPort, req.QbitPort, b2i(req.InSync), b2i(req.VpnUp), b2i(req.QbitRunning), req.Note, time.Now().Unix(),
	); err != nil {
		s.logger.Error().Err(err).Msg("port-sync status: upsert failed")
		http.Error(w, "store failed", http.StatusInternalServerError)
		return
	}
	writeJSON(w, http.StatusAccepted, map[string]any{"ok": true})
}

// handlePortSyncStatus returns the latest status for the dashboard (WebAuthn-gated).
func (s *Server) handlePortSyncStatus(w http.ResponseWriter, r *http.Request) {
	if s.sqlite == nil {
		http.Error(w, "unavailable", http.StatusServiceUnavailable)
		return
	}
	var resp portSyncStatusResp
	var inSync, vpnUp, qbitRunning int
	var note sql.NullString
	err := s.sqlite.QueryRowContext(r.Context(), `
		SELECT proton_port, qbit_port, in_sync, vpn_up, qbit_running, note, reported_at
		FROM port_sync_status WHERE id = 1`).
		Scan(&resp.ProtonPort, &resp.QbitPort, &inSync, &vpnUp, &qbitRunning, &note, &resp.ReportedAt)
	if err == sql.ErrNoRows {
		writeJSON(w, http.StatusOK, portSyncStatusResp{}) // no report yet
		return
	}
	if err != nil {
		http.Error(w, "query failed", http.StatusInternalServerError)
		return
	}
	resp.InSync, resp.VpnUp, resp.QbitRunning = inSync == 1, vpnUp == 1, qbitRunning == 1
	resp.Note = note.String
	writeJSON(w, http.StatusOK, resp)
}

func b2i(b bool) int {
	if b {
		return 1
	}
	return 0
}
