package scheduler

import (
	"context"
	"strconv"

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

// provisionSvc and provisionResolver are the slices of provision.Service /
// provision.Store the adapter needs. Declaring them as interfaces (rather than
// taking the concrete types) keeps the type-mapping logic unit-testable with
// fakes. *provision.Service satisfies provisionSvc; provision.Store satisfies
// provisionResolver.
type provisionSvc interface {
	CreateNode(ctx context.Context, spec provision.NodeSpec, idempotencyKey string) (*provision.Node, error)
	DestroyNode(ctx context.Context, id int64) error
	RotateIP(ctx context.Context, id int64) (*provision.Node, error)
	ListProviders(ctx context.Context, pool provision.Pool) ([]*provision.Provider, error)
}

type provisionResolver interface {
	GetNodeByUUID(ctx context.Context, uuid string) (*provision.Node, error)
}

// provisionAdapter implements orchestrate.ProvisionService over the real
// provision.Service (#14). The orchestrate interface keys nodes by string ID
// (node UUID); provision keys by int64. The adapter bridges that, plus the
// NodeSpec / Provider type differences.
type provisionAdapter struct {
	svc   provisionSvc
	store provisionResolver
}

// NewProvisionAdapter wires the adapter. svc is the provision service; store
// resolves a node UUID → its int64 id (provision.Store).
func NewProvisionAdapter(svc provisionSvc, store provisionResolver) orchestrate.ProvisionService {
	return &provisionAdapter{svc: svc, store: store}
}

// CreateNode maps the spec and returns the new node's UUID as the orchestrate ID.
func (a *provisionAdapter) CreateNode(ctx context.Context, spec orchestrate.NodeSpec, idempotencyKey string) (string, error) {
	var providerID int64
	if spec.ProviderID != "" {
		providerID, _ = strconv.ParseInt(spec.ProviderID, 10, 64)
	}
	n, err := a.svc.CreateNode(ctx, provision.NodeSpec{
		Region:     spec.Region,
		Role:       provision.Role(spec.Role),
		Tier:       provision.Tier(spec.Tier),
		ProviderID: providerID,
		NameZH:     spec.NameZH,
		NameEn:     spec.NameEn,
		RealitySNI: spec.RealitySNI,
		RealityPBK: spec.RealityPBK,
		HY2Port:    spec.HY2Port,
	}, idempotencyKey)
	if err != nil {
		return "", err
	}
	return n.UUID, nil
}

// DestroyNode resolves the UUID to an int64 id, then tears the node down.
func (a *provisionAdapter) DestroyNode(ctx context.Context, nodeID string) error {
	id, err := a.resolveID(ctx, nodeID)
	if err != nil {
		return err
	}
	return a.svc.DestroyNode(ctx, id)
}

// RotateIP swaps the node's IP in place; the UUID is unchanged, so it is returned.
func (a *provisionAdapter) RotateIP(ctx context.Context, nodeID string) (string, error) {
	id, err := a.resolveID(ctx, nodeID)
	if err != nil {
		return "", err
	}
	n, err := a.svc.RotateIP(ctx, id)
	if err != nil {
		return "", err
	}
	return n.UUID, nil
}

// ListProviders maps tier → pool, lists providers, and filters by region.
func (a *provisionAdapter) ListProviders(ctx context.Context, tier, region string) ([]orchestrate.ProviderInfo, error) {
	ps, err := a.svc.ListProviders(ctx, poolForTier(tier))
	if err != nil {
		return nil, err
	}
	out := make([]orchestrate.ProviderInfo, 0, len(ps))
	for _, p := range ps {
		if !p.Enabled {
			continue
		}
		if region != "" && !regionSupported(p.Regions, region) {
			continue
		}
		out = append(out, orchestrate.ProviderInfo{
			ID:      strconv.FormatInt(p.ID, 10),
			Regions: p.Regions,
		})
	}
	return out, nil
}

func (a *provisionAdapter) resolveID(ctx context.Context, uuid string) (int64, error) {
	n, err := a.store.GetNodeByUUID(ctx, uuid)
	if err != nil {
		return 0, err
	}
	return n.ID, nil
}

// poolForTier mirrors provision's tier→pool mapping: pro → premium, else consumable.
func poolForTier(tier string) provision.Pool {
	if provision.Tier(tier) == provision.TierPro {
		return provision.PoolPremium
	}
	return provision.PoolConsumable
}

// regionSupported reports whether region is in regions, treating an empty list
// as "all regions supported".
func regionSupported(regions []string, region string) bool {
	if len(regions) == 0 {
		return true
	}
	for _, r := range regions {
		if r == region {
			return true
		}
	}
	return false
}
