package hetznerapi

import (
	"context"
	"fmt"
	"sync"
)

// Fake is an in-memory CloudAPI for tests (no live Hetzner calls). It records created resources by label
// (so idempotency lookups work) and lets a test force a failure. Actions it returns are already-terminal
// (success unless FailCreate/FailAction is set), so WaitAction resolves without a network poll.
type Fake struct {
	mu sync.Mutex

	Subaccounts map[int64]Subaccount // by id
	Boxes       map[int64]StorageBox // by id
	nextID      int64

	// Records of calls (for assertions).
	CreatedSubaccounts int
	CreatedBoxes       int
	ResetCalls         int
	BoxResetCalls      int
	DeletedSubaccounts int
	DeletedBoxes       int
	GetBoxCalls        int // v0.64.0: the fetch-throttle assertion (Scenario A) counts these

	// Failure injection.
	FailCreate error // if set, CreateSubaccount/CreateStorageBox return this error
	FailAction bool  // if set, created actions come back status "error" (WaitAction fails)
	FailGetBox error // v0.64.0: if set, GetStorageBox returns this (the box-poll failure path, Scenario D)
}

// NewFake returns an empty Fake.
func NewFake() *Fake {
	return &Fake{Subaccounts: map[int64]Subaccount{}, Boxes: map[int64]StorageBox{}, nextID: 1000}
}

func (f *Fake) newAction(cmd string) Action {
	if f.FailAction {
		a := Action{ID: f.nextID, Command: cmd, Status: "error"}
		a.Error = &struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		}{Code: "action_failed", Message: "injected action failure"}
		f.nextID++
		return a
	}
	a := Action{ID: f.nextID, Command: cmd, Status: "success", Progress: 100}
	f.nextID++
	return a
}

func labelMatch(labels map[string]string, selector string) bool {
	if selector == "" {
		return true
	}
	// minimal k=v matcher (the hub only uses single-key equality selectors)
	for i := 0; i < len(selector); i++ {
		if selector[i] == '=' {
			k, v := selector[:i], selector[i+1:]
			return labels[k] == v
		}
	}
	return false
}

func (f *Fake) ListSubaccounts(_ context.Context, boxID int64, sel string) ([]Subaccount, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	var out []Subaccount
	for _, s := range f.Subaccounts {
		if s.StorageBox == boxID && labelMatch(s.Labels, sel) {
			out = append(out, s)
		}
	}
	return out, nil
}

func (f *Fake) GetSubaccount(_ context.Context, _ int64, subID int64) (Subaccount, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	s, ok := f.Subaccounts[subID]
	if !ok {
		return Subaccount{}, fmt.Errorf("hetznerapi(fake): subaccount %d not found", subID)
	}
	return s, nil
}

func (f *Fake) CreateSubaccount(_ context.Context, boxID int64, req CreateSubaccountRequest) (int64, Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.FailCreate != nil {
		return 0, Action{}, f.FailCreate
	}
	f.CreatedSubaccounts++
	id := f.nextID
	f.nextID++
	f.Subaccounts[id] = Subaccount{
		ID: id, StorageBox: boxID,
		Username:       fmt.Sprintf("u629193-sub%d", id),
		Server:         fmt.Sprintf("u629193-sub%d.your-storagebox.de", id),
		HomeDirectory:  req.HomeDirectory,
		AccessSettings: req.AccessSettings,
		Labels:         req.Labels,
	}
	return id, f.newAction("create_subaccount"), nil
}

func (f *Fake) ResetSubaccountPassword(_ context.Context, _, _ int64, _ string) (Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.ResetCalls++
	return f.newAction("reset_subaccount_password"), nil
}

func (f *Fake) UpdateSubaccountAccess(_ context.Context, _, subID int64, as AccessSettings) (Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if s, ok := f.Subaccounts[subID]; ok {
		s.AccessSettings = as
		f.Subaccounts[subID] = s
	}
	return f.newAction("update_access_settings"), nil
}

func (f *Fake) DeleteSubaccount(_ context.Context, _, subID int64) (Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	delete(f.Subaccounts, subID)
	f.DeletedSubaccounts++
	return f.newAction("delete_subaccount"), nil
}

func (f *Fake) ListStorageBoxes(_ context.Context, sel string) ([]StorageBox, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	var out []StorageBox
	for _, b := range f.Boxes {
		if labelMatch(b.Labels, sel) {
			out = append(out, b)
		}
	}
	return out, nil
}

func (f *Fake) GetStorageBox(_ context.Context, boxID int64) (StorageBox, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.GetBoxCalls++
	if f.FailGetBox != nil {
		return StorageBox{}, f.FailGetBox
	}
	b, ok := f.Boxes[boxID]
	if !ok {
		return StorageBox{}, fmt.Errorf("hetznerapi(fake): box %d not found", boxID)
	}
	return b, nil
}

func (f *Fake) CreateStorageBox(_ context.Context, req CreateBoxRequest) (int64, Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	if f.FailCreate != nil {
		return 0, Action{}, f.FailCreate
	}
	f.CreatedBoxes++
	id := f.nextID
	f.nextID++
	f.Boxes[id] = StorageBox{
		ID: id, Name: req.Name, Status: "active",
		Username: fmt.Sprintf("u6294%d", id),
		Server:   fmt.Sprintf("u6294%d.your-storagebox.de", id),
		Labels:   req.Labels,
	}
	return id, f.newAction("create_storage_box"), nil
}

func (f *Fake) ChangeType(_ context.Context, _ int64, _ string) (Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.newAction("change_type"), nil
}

func (f *Fake) ResetBoxPassword(_ context.Context, _ int64, _ string) (Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.BoxResetCalls++
	return f.newAction("reset_password"), nil
}

func (f *Fake) DeleteStorageBox(_ context.Context, boxID int64) (Action, error) {
	f.mu.Lock()
	defer f.mu.Unlock()
	delete(f.Boxes, boxID)
	f.DeletedBoxes++
	return f.newAction("delete_storage_box"), nil
}

// WaitAction resolves based on the (already-terminal) fake action status — no polling.
func (f *Fake) WaitAction(_ context.Context, a Action) error {
	if a.Status == "error" {
		return actionErr(a)
	}
	return nil
}
