package db

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/icholy/digest"

	"github.com/tidbcloud/controlplane9/internal/config"
	"github.com/tidbcloud/controlplane9/internal/verbose"
)

const (
	// starterAPIBase is the TiDB Cloud Serverless API base URL.
	starterAPIBase = "https://serverless.tidbapi.com/v1beta1"

	// starterDefaultRegion is the default region for Serverless clusters.
	starterDefaultRegion = "regions/aws-us-west-2"
)

// StarterProvider provisions TiDB Cloud Serverless instances.
// Requires an API key for authentication in "public:private" format.
// Authentication uses HTTP Digest Auth per the TiDB Cloud API spec.
type StarterProvider struct {
	// NamePrefix is the cp9 namespace prefix for resource naming (e.g. "cp9-alice").
	NamePrefix string
	APIKey     string
	// Region is the TiDB Cloud region (e.g. "regions/aws-us-west-2").
	// If empty, defaults to starterDefaultRegion.
	Region string
	// APIBase allows overriding the API URL for testing.
	APIBase string
	// Client allows injecting a custom HTTP client for testing.
	Client *http.Client
	// PollInterval controls status polling cadence. Zero uses the production default.
	PollInterval time.Duration
}

func (s *StarterProvider) apiBase() string {
	if s.APIBase != "" {
		return s.APIBase
	}
	return starterAPIBase
}

// client returns an HTTP client configured with digest auth transport.
// If Client is injected (for testing), it is returned directly.
func (s *StarterProvider) client() *http.Client {
	if s.Client != nil {
		return s.Client
	}
	pub, priv := s.parseAPIKey()
	return &http.Client{
		Timeout: 30 * time.Second,
		Transport: &digest.Transport{
			Username:  pub,
			Password:  priv,
			Transport: verbose.WrapTransport(http.DefaultTransport),
		},
	}
}

// parseAPIKey splits the API key into public and private parts.
// Format: "public:private"
func (s *StarterProvider) parseAPIKey() (public, private string) {
	pub, priv, ok := strings.Cut(s.APIKey, ":")
	if !ok {
		return s.APIKey, ""
	}
	return pub, priv
}

// Name returns the provider identifier.
func (s *StarterProvider) Name() config.DatabaseProvider {
	return config.DBTiDBCloudStarter
}

// starterCreateRequest is the request body for creating a Serverless cluster.
type starterCreateRequest struct {
	DisplayName  string        `json:"displayName"`
	Region       starterRegion `json:"region"`
	RootPassword string        `json:"rootPassword"`
}

type starterRegion struct {
	Name string `json:"name"`
}

// starterCreateResponse is the response from creating/getting a Serverless cluster.
type starterCreateResponse struct {
	ClusterID   string           `json:"clusterId"`
	DisplayName string           `json:"displayName"`
	State       string           `json:"state"`
	UserPrefix  string           `json:"userPrefix"`
	Endpoints   starterEndpoints `json:"endpoints"`
}

type starterEndpoints struct {
	Public starterPublicEndpoint `json:"public"`
}

type starterPublicEndpoint struct {
	Host string `json:"host"`
	Port int    `json:"port"`
}

// Provision creates a new TiDB Cloud Serverless cluster and waits for it to become ACTIVE.
func (s *StarterProvider) Provision(ctx context.Context) (*Instance, error) {
	if s.APIKey == "" {
		return nil, fmt.Errorf("TiDB Cloud API key is required for starter provider (set TIDB_CLOUD_API_KEY=public:private)")
	}

	region := s.Region
	if region == "" {
		region = starterDefaultRegion
	}

	rootPassword, err := generatePassword()
	if err != nil {
		return nil, fmt.Errorf("generate root password: %w", err)
	}

	// Create the cluster.
	result, err := s.createCluster(ctx, region, rootPassword)
	if err != nil {
		return nil, err
	}

	if result.ClusterID == "" {
		return nil, fmt.Errorf("starter API returned empty clusterId")
	}

	log.Printf("db: starter cluster %s created (state=%s), waiting for ACTIVE...", result.ClusterID, result.State)

	// Poll until the cluster is ACTIVE and endpoints are available.
	// Note: userPrefix is only populated once the cluster reaches ACTIVE state.
	result, err = s.waitForActive(ctx, result.ClusterID)
	if err != nil {
		return nil, fmt.Errorf("wait for cluster ready: %w", err)
	}

	if result.UserPrefix == "" {
		return nil, fmt.Errorf("starter API returned empty userPrefix for cluster %s", result.ClusterID)
	}

	port := result.Endpoints.Public.Port
	if port == 0 {
		port = 4000 // default TiDB port
	}

	// The SQL username is "<userPrefix>.root".
	user := result.UserPrefix + ".root"

	return &Instance{
		Provider: config.DBTiDBCloudStarter,
		ID:       result.ClusterID,
		Host:     result.Endpoints.Public.Host,
		Port:     port,
		User:     user,
		Password: rootPassword,
	}, nil
}

// generateDisplayName creates a unique cluster name with a random suffix.
// Uses the provider's NamePrefix to scope the name (e.g. "cp9-alice-dev-a1b2c3d4").
func (s *StarterProvider) generateDisplayName() (string, error) {
	b := make([]byte, 4)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return s.NamePrefix + "-dev-" + hex.EncodeToString(b), nil
}

// createCluster sends the POST request to create a Serverless cluster.
func (s *StarterProvider) createCluster(ctx context.Context, region, rootPassword string) (*starterCreateResponse, error) {
	displayName, err := s.generateDisplayName()
	if err != nil {
		return nil, fmt.Errorf("generate display name: %w", err)
	}

	body, err := json.Marshal(starterCreateRequest{
		DisplayName:  displayName,
		Region:       starterRegion{Name: region},
		RootPassword: rootPassword,
	})
	if err != nil {
		return nil, fmt.Errorf("marshal starter request: %w", err)
	}

	url := s.apiBase() + "/clusters"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create starter request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := s.client().Do(req)
	if err != nil {
		return nil, fmt.Errorf("starter API request: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read starter response: %w", err)
	}

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("starter API returned %d: %s", resp.StatusCode, string(respBody))
	}

	var result starterCreateResponse
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("parse starter response: %w", err)
	}

	return &result, nil
}

// waitForActive polls the cluster status until it reaches ACTIVE state.
func (s *StarterProvider) waitForActive(ctx context.Context, clusterID string) (*starterCreateResponse, error) {
	url := fmt.Sprintf("%s/clusters/%s", s.apiBase(), clusterID)
	pollInterval := s.PollInterval
	if pollInterval <= 0 {
		pollInterval = 5 * time.Second
	}

	for {
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
		if err != nil {
			return nil, fmt.Errorf("build get-cluster request: %w", err)
		}

		resp, err := s.client().Do(req)
		if err != nil {
			if ctxErr := ctx.Err(); ctxErr != nil {
				return nil, fmt.Errorf("context cancelled while waiting for cluster %s: %w", clusterID, ctxErr)
			}
			return nil, fmt.Errorf("get-cluster request: %w", err)
		}

		respBody, err := io.ReadAll(resp.Body)
		_ = resp.Body.Close()
		if err != nil {
			return nil, fmt.Errorf("read get-cluster response: %w", err)
		}

		if resp.StatusCode != http.StatusOK {
			return nil, fmt.Errorf("get-cluster returned %d: %s", resp.StatusCode, string(respBody))
		}

		var result starterCreateResponse
		if err := json.Unmarshal(respBody, &result); err != nil {
			return nil, fmt.Errorf("parse get-cluster response: %w", err)
		}

		log.Printf("db: starter cluster %s state=%s", clusterID, result.State)

		if result.State == "ACTIVE" {
			if result.Endpoints.Public.Host == "" {
				return nil, fmt.Errorf("cluster %s is ACTIVE but has no public endpoint", clusterID)
			}
			return &result, nil
		}

		if result.State == "DELETED" {
			return nil, fmt.Errorf("cluster %s was deleted during provisioning", clusterID)
		}

		timer := time.NewTimer(pollInterval)
		select {
		case <-ctx.Done():
			timer.Stop()
			return nil, fmt.Errorf("context cancelled while waiting for cluster %s: %w", clusterID, ctx.Err())
		case <-timer.C:
		}
	}
}

// CreateDatabase creates a named database within the Starter instance
// using the MySQL wire protocol.
func (s *StarterProvider) CreateDatabase(ctx context.Context, inst *Instance, dbName string) error {
	return createDatabaseMySQL(ctx, inst, dbName)
}

// starterListResponse is the response from listing Serverless clusters.
type starterListResponse struct {
	Clusters []starterCreateResponse `json:"clusters"`
}

// CleanupStale lists all clusters and deletes any with the cp9-dev- display name prefix
// that are not in CREATING state (to avoid racing with in-progress provisioning).
// Only runs when the CI environment variable is set.
func (s *StarterProvider) CleanupStale(ctx context.Context) {
	if s.APIKey == "" {
		return
	}

	url := s.apiBase() + "/clusters"
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
	if err != nil {
		return
	}

	resp, err := s.client().Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return
	}

	var list starterListResponse
	if err := json.Unmarshal(respBody, &list); err != nil {
		return
	}

	for _, c := range list.Clusters {
		if c.DisplayName == "" || c.ClusterID == "" {
			continue
		}
		if !strings.HasPrefix(c.DisplayName, s.NamePrefix+"-dev-") {
			continue
		}
		if c.State == "DELETED" || c.State == "CREATING" {
			continue
		}
		log.Printf("db: cleaning up stale cluster %s (%s, state=%s)", c.ClusterID, c.DisplayName, c.State)
		inst := &Instance{ID: c.ClusterID}
		if delErr := s.Destroy(ctx, inst); delErr != nil {
			log.Printf("db: cleanup failed for %s: %v", c.ClusterID, delErr)
		}
	}
}

// Destroy deletes a TiDB Cloud Serverless cluster.
func (s *StarterProvider) Destroy(ctx context.Context, inst *Instance) error {
	if s.APIKey == "" {
		return fmt.Errorf("TiDB Cloud API key is required to destroy starter instance (set TIDB_CLOUD_API_KEY=public:private)")
	}
	url := fmt.Sprintf("%s/clusters/%s", s.apiBase(), inst.ID)
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, http.NoBody)
	if err != nil {
		return fmt.Errorf("create delete request: %w", err)
	}
	// Auth is handled by the digest transport.

	resp, err := s.client().Do(req)
	if err != nil {
		return fmt.Errorf("starter delete request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
		respBody, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("starter delete returned %d: %s", resp.StatusCode, string(respBody))
	}
	return nil
}

// CleanupStaleStarter deletes any leftover cp9-*-dev-* Serverless clusters.
// Only performs cleanup when running in CI (CI env var is set).
func CleanupStaleStarter(ctx context.Context, apiKey, namePrefix string) {
	if os.Getenv("CI") == "" {
		return
	}
	p := &StarterProvider{NamePrefix: namePrefix, APIKey: apiKey}
	p.CleanupStale(ctx)
}

// generatePassword creates a random 16-byte hex password for Serverless clusters.
func generatePassword() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}
