package report

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
)

// PushStatus tracks the last hub push attempt and result.
type PushStatus struct {
	LastAttempt time.Time
	LastSuccess time.Time
	LastError   string
	Consecutive int // consecutive failures
}

// PushResponse is the parsed response from the Hub after a report push.
type PushResponse struct {
	Status          string `json:"status"`
	CustomerBlocked bool   `json:"customer_blocked"`
	// Phase 2 managed updates: the effective controller-version FLOOR (the operator's enforced
	// minimum) and the latest available version. Empty when the hub has none configured / is old.
	// The controller auto-updates to the floor when below it (latest stays the customer's opt-in
	// "update to latest" button — never the auto-target).
	MinControllerVersion string `json:"min_controller_version"`
	LatestVersion        string `json:"latest_version"`
	// ConfigVersion is the hub's per-customer config counter (v0.26.0). On a change vs. the
	// last-applied version, the controller re-pulls controller.yaml + self-restarts (pull-based config
	// delivery — the hub never connects into the box). 0 = the hub didn't advertise it (old hub, or a
	// report-only customer with no config row) → the controller does nothing.
	ConfigVersion int `json:"config_version"`
}

// Pusher sends reports to the central hub.
type Pusher struct {
	hubURL     string
	apiKey     string
	httpClient *http.Client
	logger     *log.Logger
	enabled    bool
	debug      bool

	statusMu sync.RWMutex
	status   PushStatus

	// OnPushResponse is called after each successful report push with the parsed response.
	// Set by main.go to update hub verification state.
	OnPushResponse func(resp *PushResponse)
}

// NewPusher creates a new report pusher from hub configuration.
func NewPusher(cfg *config.HubConfig, logger *log.Logger, debug bool) *Pusher {
	return &Pusher{
		hubURL: strings.TrimRight(cfg.URL, "/"),
		apiKey: cfg.APIKey,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
		logger:  logger,
		enabled: cfg.Enabled,
		debug:   debug,
	}
}

// Push sends a report to the hub. Retries 3 times with 5s backoff.
func (p *Pusher) Push(report *Report) error {
	if !p.enabled {
		return nil
	}

	data, err := json.Marshal(report)
	if err != nil {
		return fmt.Errorf("marshal report: %w", err)
	}

	url := p.hubURL + "/api/v1/report"
	if p.debug {
		p.logger.Printf("[DEBUG] [report] Push: url=%s payload=%d bytes", url, len(data))
	}

	p.statusMu.Lock()
	p.status.LastAttempt = time.Now()
	p.statusMu.Unlock()

	var lastErr error
	for attempt := 0; attempt < 3; attempt++ {
		if attempt > 0 {
			time.Sleep(5 * time.Second)
		}

		req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
		if err != nil {
			lastErr = err
			continue
		}
		req.Header.Set("Content-Type", "application/json")
		if p.apiKey != "" {
			req.Header.Set("Authorization", "Bearer "+p.apiKey)
		}

		resp, err := p.httpClient.Do(req)
		if err != nil {
			lastErr = err
			continue
		}

		// Read response body to parse customer_blocked field
		respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
		resp.Body.Close()

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			p.logger.Printf("[INFO] [report] Hub report pushed successfully (%d bytes)", len(data))
			p.statusMu.Lock()
			p.status.LastSuccess = time.Now()
			p.status.LastError = ""
			p.status.Consecutive = 0
			p.statusMu.Unlock()

			// Parse response for customer_blocked field
			if p.OnPushResponse != nil && len(respBody) > 0 {
				var pr PushResponse
				if json.Unmarshal(respBody, &pr) == nil {
					p.OnPushResponse(&pr)
				}
			}
			return nil
		}
		lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
	}

	p.logger.Printf("[WARN] [report] Push failed: %v", lastErr)

	p.statusMu.Lock()
	p.status.LastError = lastErr.Error()
	p.status.Consecutive++
	p.statusMu.Unlock()

	return fmt.Errorf("hub push failed after 3 attempts: %w", lastErr)
}

// GetStatus returns a snapshot of the current push status.
func (p *Pusher) GetStatus() PushStatus {
	p.statusMu.RLock()
	defer p.statusMu.RUnlock()
	return p.status
}

// (PushInfraBackup removed 2026-06-16 — the infra-backup mechanism was retired hub-side.
// It was dead since slice 8C, had no callers, and pushed plaintext secrets to the hub.)

// PushOnce sends a single report regardless of the enabled flag.
// Used for one-time notifications (e.g., reporting-disabled on startup).
func (p *Pusher) PushOnce(report *Report) error {
	if p.hubURL == "" || p.apiKey == "" {
		return nil
	}

	data, err := json.Marshal(report)
	if err != nil {
		return fmt.Errorf("marshal report: %w", err)
	}

	url := p.hubURL + "/api/v1/report"

	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
	if err != nil {
		return fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+p.apiKey)

	resp, err := p.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("hub push-once: %w", err)
	}
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()

	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		p.logger.Printf("[INFO] [report] Hub push-once sent (%d bytes)", len(data))
		return nil
	}
	return fmt.Errorf("hub push-once: HTTP %d", resp.StatusCode)
}
