package controlplane

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/pingcap-inc/labs.tidb.io/ssh-proxy/internal/config"
	"github.com/pingcap/log"
	"go.uber.org/zap"
)

const (
	registerPath = "/api/v2/ssh-servers/registration"
	reportPath   = "/api/v2/ssh-servers/%d/report"
)

type Client struct {
	domain           string
	preferredRegions []string
	sshServerId      int
	baseURL          string
	apiKey           string
	httpClient       *http.Client
}

type StatusReport struct {
	Score                    int   `json:"score"`
	ActiveWebConsoleSessions int64 `json:"active_session"`
}

func NewClient(cfg *config.Config) *Client {
	return &Client{
		domain:           cfg.Domain,
		preferredRegions: cfg.PreferredRegions,
		baseURL:          cfg.LabBaseURL,
		apiKey:           cfg.LabApiKey,
		httpClient: &http.Client{
			Timeout: 5 * time.Second,
		},
	}
}

func (c *Client) doPost(ctx context.Context, path string, payload any) (*http.Response, error) {
	base := strings.TrimRight(c.baseURL, "/")
	p := path
	if !strings.HasPrefix(p, "/") {
		p = "/" + p
	}
	url := base + p
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("json.Marshal: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("http.NewRequestWithContext: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	if c.apiKey != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
	}
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("httpClient.Do: %w", err)
	}
	return resp, nil
}

func (c *Client) Register(ctx context.Context) error {
	payload := struct {
		Domain  string   `json:"domain"`
		Regions []string `json:"regions"`
	}{
		Domain:  c.domain,
		Regions: c.preferredRegions,
	}
	resp, err := c.doPost(ctx, registerPath, payload)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}
	var res struct {
		ID int `json:"id"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
		return fmt.Errorf("decode register response: %w", err)
	}
	if res.ID <= 0 {
		return fmt.Errorf("invalid sshServerId in response: %d", res.ID)
	}
	c.sshServerId = res.ID
	log.Info("registered ssh server to control plane", zap.Int("serverId", c.sshServerId))
	return nil
}

func (c *Client) ReportStatus(ctx context.Context, report StatusReport) error {
	resp, err := c.doPost(ctx, fmt.Sprintf(reportPath, c.sshServerId), report)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
	}
	return nil
}

// StartReporter starts a periodic status reporter until ctx is cancelled.
// getActive should return the current number of active web console sessions.
func (c *Client) StartReporter(ctx context.Context, getActive func() int64, interval time.Duration) {
	if interval <= 0 {
		interval = 5 * time.Second
	}
	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	// fire once immediately
	c.reportOnce(ctx, getActive)

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			c.reportOnce(ctx, getActive)
		}
	}
}

func (c *Client) reportOnce(ctx context.Context, getActive func() int64) {
	ctxReport, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	report := StatusReport{
		Score:                    100, // TODO: calculate score
		ActiveWebConsoleSessions: getActive(),
	}
	if err := c.ReportStatus(ctxReport, report); err != nil {
		log.Warn("failed to report status", zap.Error(err))
	}
}
