package provisioning

import (
	"context"
	"fmt"
	"sync/atomic"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/warmbly/warmbly/internal/infrastructure/cloudprovider"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/repository"
)

// Runner is the background loop that actually drives provisioning jobs.
//
// Previously nothing called Service.Run, so a job created from the admin UI sat
// in "pending" forever. The Runner polls for in-flight jobs and drives each one
// to a terminal state (completed/failed). Run is idempotent and resumes from a
// job's current state, so a backend restart mid-provision is recoverable.
type Runner struct {
	Jobs     repository.ProvisioningJobRepository
	Svc      *Service
	Interval time.Duration // default 15s
	// DryRun is informational — it only affects the boot log line. The actual
	// dry-run behaviour comes from the Service's provider/installer wiring.
	DryRun bool
}

// Run blocks until ctx is cancelled, processing in-flight jobs on each tick.
func (r *Runner) Run(ctx context.Context) {
	interval := r.Interval
	if interval == 0 {
		interval = 15 * time.Second
	}
	log.Info().
		Bool("dry_run", r.DryRun).
		Dur("interval", interval).
		Msg("provisioning runner started")

	tick := time.NewTicker(interval)
	defer tick.Stop()
	r.processOnce(ctx)
	for {
		select {
		case <-ctx.Done():
			return
		case <-tick.C:
			r.processOnce(ctx)
		}
	}
}

func (r *Runner) processOnce(ctx context.Context) {
	if r.Svc == nil || r.Jobs == nil {
		return
	}
	jobs, err := r.Jobs.ListInFlight(ctx)
	if err != nil {
		log.Warn().Err(err).Msg("provisioning runner: list in-flight failed")
		return
	}
	for i := range jobs {
		job := jobs[i]
		// rolling_back is a transient state the Service sets just before it
		// fails a job; skip it so we don't log a spurious "already terminal".
		if job.State == models.ProvJobRollingBack {
			continue
		}
		if err := r.Svc.Run(ctx, job.ID); err != nil {
			log.Warn().Err(err).Str("job", job.ID.String()).Msg("provisioning job failed")
		}
	}
}

// ---------------------------------------------------------------------------
// Dry-run provider
// ---------------------------------------------------------------------------

var dryRunSeq atomic.Uint64

// DryRunProvider implements cloudprovider.Provider without touching any real
// cloud API. It returns plausible fake server/IP identifiers so the state
// machine can run end-to-end in local dev (and in any environment where
// PROVISIONING_DRY_RUN is on) without creating — or billing — real machines.
type DryRunProvider struct{}

func (DryRunProvider) Name() string { return "dry-run" }

func (DryRunProvider) Locations(context.Context) ([]cloudprovider.Location, error) {
	return []cloudprovider.Location{}, nil
}
func (DryRunProvider) ServerTypes(context.Context) ([]cloudprovider.ServerType, error) {
	return []cloudprovider.ServerType{}, nil
}
func (DryRunProvider) Images(context.Context) ([]cloudprovider.Image, error) {
	return []cloudprovider.Image{}, nil
}
func (DryRunProvider) Verify(context.Context) error { return nil }

func (DryRunProvider) CreateServer(_ context.Context, req cloudprovider.CreateServerRequest) (*cloudprovider.Server, error) {
	n := dryRunSeq.Add(1)
	return &cloudprovider.Server{
		ID:         fmt.Sprintf("dryrun-%d", n),
		Name:       req.Name,
		Status:     "running",
		PublicIPv4: dryRunIP(n),
	}, nil
}

func (DryRunProvider) DeleteServer(context.Context, string) error { return nil }

func (DryRunProvider) CreatePrimaryIP(_ context.Context, req cloudprovider.CreatePrimaryIPRequest) (*cloudprovider.PrimaryIP, error) {
	n := dryRunSeq.Add(1)
	return &cloudprovider.PrimaryIP{
		ID:   fmt.Sprintf("dryrun-ip-%d", n),
		Type: req.Type,
		IP:   dryRunIP(n),
	}, nil
}

func (DryRunProvider) AssignPrimaryIP(context.Context, string, string) error { return nil }
func (DryRunProvider) UnassignPrimaryIP(context.Context, string) error       { return nil }
func (DryRunProvider) DeletePrimaryIP(context.Context, string) error         { return nil }
func (DryRunProvider) SetReverseDNS(context.Context, string, string) error   { return nil }

// dryRunIP maps a sequence number into a deterministic 10.x.x.x address so the
// inet[] columns get a valid value.
func dryRunIP(n uint64) string {
	return fmt.Sprintf("10.%d.%d.%d", (n>>16)&0xff, (n>>8)&0xff, (n&0xfe)+1)
}

var _ cloudprovider.Provider = DryRunProvider{}
