package admin

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"strconv"
	"time"

	"github.com/wangjia/pangolin/server/internal/codes"
	"github.com/wangjia/pangolin/server/internal/provision"
	"github.com/wangjia/pangolin/server/internal/scheduler"
	"github.com/wangjia/pangolin/server/internal/scheduler/orchestrate"
)

// ErrServiceUnavailable is returned by stub services whose real implementation
// (#5 lifecycle, #14 provisioning) is not yet wired in. The UI greys out the
// corresponding controls when Ready() is false.
var ErrServiceUnavailable = errors.New("admin: service not available yet")

// --------------------------------------------------------------------------
// Code-batch service (#3 codes — implemented)
// --------------------------------------------------------------------------

// CodeBatchParams are the inputs to a batch generation.
type CodeBatchParams struct {
	Plan         string
	DurationDays int
	Count        int
	Channel      string
	Note         string
	CreatedBy    string
}

// GeneratedBatch carries the one-and-only plaintext output of a generation.
type GeneratedBatch struct {
	BatchID      int64
	Plan         string
	DurationDays int
	Channel      string
	Codes        []string // plaintext, streamed to CSV once and never stored
	GeneratedAt  time.Time
}

// CodesService is the subset of the #3 codes service the admin UI consumes.
type CodesService interface {
	CreateBatch(ctx context.Context, p CodeBatchParams) (*GeneratedBatch, error)
	ListBatches(ctx context.Context, limit, offset int) ([]BatchSummary, int, error)
	VoidBatch(ctx context.Context, batchID int64) (int64, error)
}

// CodesAdapter adapts the real codes.Service / codes.Store to CodesService.
type CodesAdapter struct {
	svc   *codes.Service
	store *codes.Store
}

// NewCodesAdapter wires the real codes implementation.
func NewCodesAdapter(svc *codes.Service, store *codes.Store) *CodesAdapter {
	return &CodesAdapter{svc: svc, store: store}
}

// CreateBatch generates a batch and returns the plaintext codes.
func (a *CodesAdapter) CreateBatch(ctx context.Context, p CodeBatchParams) (*GeneratedBatch, error) {
	res, err := a.svc.CreateBatch(ctx, codes.BatchRequest{
		PlanCode:     codes.PlanCode(p.Plan),
		DurationDays: p.DurationDays,
		Count:        p.Count,
		Channel:      codes.BatchChannel(p.Channel),
		Note:         p.Note,
		CreatedBy:    p.CreatedBy,
	})
	if err != nil {
		return nil, err
	}
	return &GeneratedBatch{
		BatchID:      res.BatchID,
		Plan:         string(res.PlanCode),
		DurationDays: res.DurationDays,
		Channel:      string(res.Channel),
		Codes:        res.Codes,
		GeneratedAt:  time.Now().UTC(),
	}, nil
}

// ListBatches returns paginated batch summaries.
func (a *CodesAdapter) ListBatches(ctx context.Context, limit, offset int) ([]BatchSummary, int, error) {
	infos, total, err := a.store.ListBatches(ctx, limit, offset)
	if err != nil {
		return nil, 0, err
	}
	out := make([]BatchSummary, len(infos))
	for i, b := range infos {
		out[i] = BatchSummary{
			ID:        b.ID,
			Channel:   string(b.Channel),
			CreatedBy: b.CreatedBy,
			Note:      b.Note,
			CreatedAt: b.CreatedAt,
			Total:     b.Total,
			Redeemed:  b.Redeemed,
			Void:      b.Void,
			Unused:    b.Unused,
		}
	}
	return out, total, nil
}

// VoidBatch voids all unused codes in a batch.
func (a *CodesAdapter) VoidBatch(ctx context.Context, batchID int64) (int64, error) {
	return a.store.VoidBatch(ctx, batchID)
}

// --------------------------------------------------------------------------
// Node lifecycle service (#5) — not yet implemented
// --------------------------------------------------------------------------

// LifecycleService transitions a node's status (draining / up). Backed by the
// #5 lifecycle module once available.
type LifecycleService interface {
	// TransitionStatus moves nodeID to target ("draining" | "up").
	TransitionStatus(ctx context.Context, nodeID int64, target, actor string) error
	// Ready reports whether the real implementation is wired (UI greys out
	// controls when false).
	Ready() bool
}

// StubLifecycle is the placeholder used until #5 lands.
type StubLifecycle struct{}

// NewStubLifecycle returns a not-ready lifecycle service.
func NewStubLifecycle() *StubLifecycle { return &StubLifecycle{} }

// TransitionStatus always fails until #5 is wired.
func (StubLifecycle) TransitionStatus(context.Context, int64, string, string) error {
	return ErrServiceUnavailable
}

// Ready reports false.
func (StubLifecycle) Ready() bool { return false }

// RealLifecycle is the SQL-backed lifecycle service. It reuses the scheduler's
// canonical optimistic-lock transition (UPDATE nodes ... WHERE status=from, plus
// a node_events row and a directory_version bump so clients re-fetch), so manual
// admin transitions and the autonomous scheduler loop share one code path.
type RealLifecycle struct {
	db *sql.DB
	lc orchestrate.LifecycleService
}

// NewRealLifecycle wires the lifecycle adapter over db.
func NewRealLifecycle(db *sql.DB) *RealLifecycle {
	return &RealLifecycle{
		db: db,
		lc: scheduler.NewOrchestrateLifecycle(scheduler.NewSQLLifecycle(db, nil)),
	}
}

// TransitionStatus moves nodeID to target ("draining" | "up"). It reads the
// current status as the optimistic-lock guard, so the transition is a safe
// no-op if the node already moved.
func (l *RealLifecycle) TransitionStatus(ctx context.Context, nodeID int64, target, actor string) error {
	if target != "draining" && target != "up" {
		return fmt.Errorf("admin: unsupported target status %q", target)
	}
	var from string
	switch err := l.db.QueryRowContext(ctx, `SELECT status FROM nodes WHERE id = ?`, nodeID).Scan(&from); err {
	case nil:
	case sql.ErrNoRows:
		return fmt.Errorf("admin: node %d not found", nodeID)
	default:
		return err
	}
	if from == target {
		return nil // already in target state — idempotent no-op
	}
	n, err := l.lc.TransitionStatus(ctx, strconv.FormatInt(nodeID, 10), from, target,
		map[string]any{"actor": actor, "source": "admin"})
	if err != nil {
		return err
	}
	if n == 0 {
		return fmt.Errorf("admin: node %d status changed concurrently", nodeID)
	}
	return nil
}

// Ready reports true — the lifecycle service only needs the database.
func (*RealLifecycle) Ready() bool { return true }

// --------------------------------------------------------------------------
// Node provisioning service (#14) — not yet implemented
// --------------------------------------------------------------------------

// ProvisionService replaces a node by provisioning a fresh one. Backed by the
// #14 ProvisionService once available.
type ProvisionService interface {
	// Replace decommissions nodeID and provisions a replacement.
	Replace(ctx context.Context, nodeID int64, actor string) error
	// Ready reports whether the real implementation is wired.
	Ready() bool
}

// StubProvision is the placeholder used until #14 lands.
type StubProvision struct{}

// NewStubProvision returns a not-ready provision service.
func NewStubProvision() *StubProvision { return &StubProvision{} }

// Replace always fails until #14 is wired.
func (StubProvision) Replace(context.Context, int64, string) error { return ErrServiceUnavailable }

// Ready reports false.
func (StubProvision) Ready() bool { return false }

// provReplacer is the slice of provision.Service the admin replace control needs
// (declared as an interface so tests can fake it without a database).
type provReplacer interface {
	Replace(ctx context.Context, nodeID int64, replacementUUID string) (*provision.ReplaceResult, error)
}

// RealProvision drives a one-click make-before-break node replacement via the
// real provision.Service (doc/04 §4.1): a fresh node is brought UP before the
// old one drains, so capacity never dips.
type RealProvision struct {
	svc provReplacer
}

// NewRealProvision wires the provision adapter over the real service.
func NewRealProvision(svc provReplacer) *RealProvision { return &RealProvision{svc: svc} }

// Replace provisions a replacement for nodeID. An empty replacementUUID starts a
// fresh orchestration (the service mints its own idempotency key). The actor is
// recorded in the admin audit log by the caller.
func (p *RealProvision) Replace(ctx context.Context, nodeID int64, _ string) error {
	_, err := p.svc.Replace(ctx, nodeID, "")
	return err
}

// Ready reports true — the provision service is wired. Without vendor
// credentials the Replace call fails with a clear error when invoked.
func (*RealProvision) Ready() bool { return true }

// Services bundles the three downstream services the admin UI depends on.
type Services struct {
	Codes     CodesService
	Lifecycle LifecycleService
	Provision ProvisionService
}
