package admin

import (
	"context"
	"strings"
	"time"
)

// --------------------------------------------------------------------------
// Fake Store
// --------------------------------------------------------------------------

type fakeStore struct {
	admins    map[string]*Admin
	nodes     []NodeRow
	events    map[int64][]NodeEvent
	audits    []AuditEntry
	lastLogin map[int64]time.Time
}

func newFakeStore() *fakeStore {
	return &fakeStore{
		admins:    map[string]*Admin{},
		events:    map[int64][]NodeEvent{},
		lastLogin: map[int64]time.Time{},
	}
}

func (f *fakeStore) GetAdminByUsername(_ context.Context, username string) (*Admin, error) {
	a, ok := f.admins[username]
	if !ok {
		return nil, ErrAdminNotFound
	}
	return a, nil
}

func (f *fakeStore) CreateAdmin(_ context.Context, username, pwHash string, enc []byte) (int64, error) {
	id := int64(len(f.admins) + 1)
	f.admins[username] = &Admin{ID: id, Username: username, PwHash: pwHash, TOTPSecretEnc: enc, Status: "active"}
	return id, nil
}

func (f *fakeStore) UpdateLastLogin(_ context.Context, id int64, at time.Time) error {
	f.lastLogin[id] = at
	return nil
}

func (f *fakeStore) ListNodes(_ context.Context, eventsPerNode int) ([]NodeRow, error) {
	out := make([]NodeRow, len(f.nodes))
	copy(out, f.nodes)
	for i := range out {
		ev := f.events[out[i].ID]
		if eventsPerNode > 0 && len(ev) > eventsPerNode {
			ev = ev[:eventsPerNode]
		}
		out[i].RecentEvents = ev
	}
	return out, nil
}

func (f *fakeStore) GetNode(_ context.Context, id int64) (*NodeRow, error) {
	for i := range f.nodes {
		if f.nodes[i].ID == id {
			n := f.nodes[i]
			return &n, nil
		}
	}
	return nil, nil
}

func (f *fakeStore) WriteAudit(_ context.Context, actor, action, target, metaJSON string) error {
	f.audits = append(f.audits, AuditEntry{
		ID: int64(len(f.audits) + 1), Actor: actor, Action: action,
		Target: target, Meta: metaJSON, At: time.Now().UTC(),
	})
	return nil
}

func (f *fakeStore) QueryAudit(_ context.Context, flt AuditFilter) ([]AuditEntry, int, error) {
	var matched []AuditEntry
	for _, e := range f.audits {
		if flt.Actor != "" && e.Actor != flt.Actor {
			continue
		}
		if flt.Action != "" && e.Action != flt.Action {
			continue
		}
		if flt.Target != "" && !strings.Contains(e.Target, flt.Target) {
			continue
		}
		matched = append(matched, e)
	}
	total := len(matched)
	off := flt.Offset
	if off > len(matched) {
		off = len(matched)
	}
	matched = matched[off:]
	if flt.Limit > 0 && len(matched) > flt.Limit {
		matched = matched[:flt.Limit]
	}
	return matched, total, nil
}

func (f *fakeStore) QueryNodeEvents(_ context.Context, nodeID int64, limit int) ([]NodeEvent, error) {
	ev := f.events[nodeID]
	if limit > 0 && len(ev) > limit {
		ev = ev[:limit]
	}
	return ev, nil
}

// auditFor returns the audit entries whose action matches.
func (f *fakeStore) auditFor(action string) []AuditEntry {
	var out []AuditEntry
	for _, e := range f.audits {
		if e.Action == action {
			out = append(out, e)
		}
	}
	return out
}

// --------------------------------------------------------------------------
// Fake CodesService
// --------------------------------------------------------------------------

type fakeCodes struct {
	batches    []BatchSummary
	created    []CodeBatchParams
	nextCodes  []string
	nextID     int64
	voided     []int64
	voidReturn int64
}

func (c *fakeCodes) CreateBatch(_ context.Context, p CodeBatchParams) (*GeneratedBatch, error) {
	c.created = append(c.created, p)
	c.nextID++
	codes := c.nextCodes
	if codes == nil {
		codes = make([]string, p.Count)
		for i := range codes {
			codes[i] = "PLAINCODE" + itoa(i)
		}
	}
	return &GeneratedBatch{
		BatchID: c.nextID, Plan: p.Plan, DurationDays: p.DurationDays,
		Channel: p.Channel, Codes: codes, GeneratedAt: time.Now().UTC(),
	}, nil
}

func (c *fakeCodes) ListBatches(_ context.Context, limit, offset int) ([]BatchSummary, int, error) {
	return c.batches, len(c.batches), nil
}

func (c *fakeCodes) VoidBatch(_ context.Context, batchID int64) (int64, error) {
	c.voided = append(c.voided, batchID)
	return c.voidReturn, nil
}

// --------------------------------------------------------------------------
// Recording Lifecycle / Provision services
// --------------------------------------------------------------------------

type lifeCall struct {
	NodeID int64
	Target string
	Actor  string
}

type recordingLifecycle struct {
	ready bool
	err   error
	calls []lifeCall
}

func (r *recordingLifecycle) TransitionStatus(_ context.Context, nodeID int64, target, actor string) error {
	r.calls = append(r.calls, lifeCall{nodeID, target, actor})
	return r.err
}
func (r *recordingLifecycle) Ready() bool { return r.ready }

type provCall struct {
	NodeID int64
	Actor  string
}

type recordingProvision struct {
	ready bool
	err   error
	calls []provCall
}

func (r *recordingProvision) Replace(_ context.Context, nodeID int64, actor string) error {
	r.calls = append(r.calls, provCall{nodeID, actor})
	return r.err
}
func (r *recordingProvision) Ready() bool { return r.ready }

func itoa(i int) string {
	if i == 0 {
		return "0"
	}
	var b [20]byte
	pos := len(b)
	for i > 0 {
		pos--
		b[pos] = byte('0' + i%10)
		i /= 10
	}
	return string(b[pos:])
}
