package clientfake

import (
	"context"
	"fmt"
	"math/rand/v2"
	"reflect"

	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
)

type fakeEtcdKV struct {
	DB map[string]string

	PutResponse     *clientv3.PutResponse
	GetResponse     *clientv3.GetResponse
	DeleteResponse  *clientv3.DeleteResponse
	CompactResponse *clientv3.CompactResponse
	OpResponse      *clientv3.OpResponse

	txn *fakeTxn

	Err, TxnErr error
}

func (fKV *fakeEtcdKV) Put(
	ctx context.Context, key, val string, opts ...clientv3.OpOption) (*clientv3.PutResponse, error) {
	if fKV.PutResponse != nil {
		return fKV.PutResponse, fKV.Err
	}

	fKV.DB[key] = val

	return &clientv3.PutResponse{}, nil
}

func (fKV *fakeEtcdKV) Get(
	ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
	if fKV.GetResponse != nil {
		return fKV.GetResponse, fKV.Err
	}

	// Apply options (e.g., WithPrefix) by materializing an Op.
	op := clientv3.OpGet(key, opts...)
	start := string(op.KeyBytes())
	endb := op.RangeBytes()

	// Single key path (no range/prefix)
	if len(endb) == 0 {
		v, ok := fKV.DB[start]
		if ok {
			return &clientv3.GetResponse{
				Kvs:   []*mvccpb.KeyValue{{Key: []byte(start), Value: []byte(v)}},
				Count: 1,
			}, nil
		}
		return &clientv3.GetResponse{Kvs: nil, Count: 0}, nil
	}

	// Range path (covers WithPrefix and explicit ranges): select keys in [start, end)
	end := string(endb)
	kvs := make([]*mvccpb.KeyValue, 0)
	for k, v := range fKV.DB {
		if k >= start && k < end {
			kvs = append(kvs, &mvccpb.KeyValue{Key: []byte(k), Value: []byte(v)})
		}
	}
	return &clientv3.GetResponse{Kvs: kvs, Count: int64(len(kvs))}, nil
}

func (fKV *fakeEtcdKV) Delete(
	ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) {
	if fKV.DeleteResponse != nil {
		return fKV.DeleteResponse, fKV.Err
	}

	// Apply options (e.g., WithPrefix) by materializing an Op.
	op := clientv3.OpDelete(key, opts...)
	start := string(op.KeyBytes())
	endb := op.RangeBytes()

	// Single key delete
	if len(endb) == 0 {
		var deleted int64
		if _, ok := fKV.DB[start]; ok {
			delete(fKV.DB, start)
			deleted = 1
		}
		return &clientv3.DeleteResponse{Deleted: deleted}, nil
	}

	// Range delete (covers WithPrefix and explicit ranges): delete keys in [start, end)
	end := string(endb)
	var deleted int64
	for k := range fKV.DB {
		if k >= start && k < end {
			delete(fKV.DB, k)
			deleted++
		}
	}
	return &clientv3.DeleteResponse{Deleted: deleted}, nil
}

func (fKV *fakeEtcdKV) Compact(
	ctx context.Context, rev int64, opts ...clientv3.CompactOption) (*clientv3.CompactResponse, error) {
	if fKV.CompactResponse != nil {
		return fKV.CompactResponse, fKV.Err
	}

	return &clientv3.CompactResponse{}, nil
}

func (fKV *fakeEtcdKV) Do(ctx context.Context, op clientv3.Op) (clientv3.OpResponse, error) {
	if fKV.OpResponse != nil {
		return *fKV.OpResponse, fKV.Err
	}

	// Best-effort support: handle simple put/get/delete ops; ignore advanced options.
	if op.IsPut() {
		_, err := fKV.Put(ctx, string(op.KeyBytes()), string(op.ValueBytes()))
		return clientv3.OpResponse{}, err
	}
	if op.IsGet() {
		_, err := fKV.Get(ctx, string(op.KeyBytes()))
		return clientv3.OpResponse{}, err
	}
	if op.IsDelete() {
		_, err := fKV.Delete(ctx, string(op.KeyBytes()))
		return clientv3.OpResponse{}, err
	}
	// Unknown op types return zero-value response with nil error.
	return clientv3.OpResponse{}, nil
}

func (fKV *fakeEtcdKV) Txn(ctx context.Context) clientv3.Txn {
	return fKV.txn
}

type BuildArg struct {
	DB map[string]string

	PutResponse     *clientv3.PutResponse
	GetResponse     *clientv3.GetResponse
	DeleteResponse  *clientv3.DeleteResponse
	CompactResponse *clientv3.CompactResponse
	OpResponse      *clientv3.OpResponse

	TxnResponse *clientv3.TxnResponse

	Err, TxnErr error
}

func NewEtcdKV(b BuildArg) clientv3.KV {
	db := make(map[string]string, len(b.DB))
	for k, v := range b.DB {
		db[k] = v
	}

	kv := &fakeEtcdKV{
		DB: db,

		PutResponse:     b.PutResponse,
		GetResponse:     b.GetResponse,
		DeleteResponse:  b.DeleteResponse,
		CompactResponse: b.CompactResponse,
		OpResponse:      b.OpResponse,

		Err: b.Err,
	}

	kv.txn = &fakeTxn{kv: kv, resp: b.TxnResponse, err: b.TxnErr}
	return kv
}

type fakeTxn struct {
	kv clientv3.KV

	// resp is only used when tests need full control over the returned TxnResponse.
	// When resp is nil, Commit() applies ops best-effort and returns a minimal response.
	resp *clientv3.TxnResponse
	err  error

	cmps []clientv3.Cmp

	thenOps []clientv3.Op
	elseOps []clientv3.Op
}

func (fTxn *fakeTxn) If(cs ...clientv3.Cmp) clientv3.Txn {
	// Store comparisons and evaluate them in Commit() to decide Then/Else.
	fTxn.cmps = append(fTxn.cmps, cs...)
	return fTxn
}

func (fTxn *fakeTxn) Then(ops ...clientv3.Op) clientv3.Txn {
	fTxn.thenOps = append(fTxn.thenOps, ops...)
	return fTxn
}

func (fTxn *fakeTxn) Else(ops ...clientv3.Op) clientv3.Txn {
	fTxn.elseOps = append(fTxn.elseOps, ops...)
	return fTxn
}

func (fTxn *fakeTxn) Commit() (*clientv3.TxnResponse, error) {
	if fTxn.resp != nil {
		return fTxn.resp, fTxn.err
	}

	defer func() {
		fTxn.elseOps = nil
		fTxn.thenOps = nil
		fTxn.cmps = nil
	}()

	ctx := context.Background()

	succeeded := true
	if len(fTxn.cmps) > 0 {
		v, err := fTxn.evalCmps(ctx)
		if err != nil {
			return nil, err
		}
		succeeded = v
	}

	ops := fTxn.thenOps
	if !succeeded {
		ops = fTxn.elseOps
	}

	resp := &clientv3.TxnResponse{Succeeded: succeeded}

	if fTxn.kv != nil {
		for _, op := range ops {
			// Record responses in the TxnResponse (similar shape to real etcd txn).
			switch {
			case op.IsPut():
				r, err := fTxn.kv.Put(ctx, string(op.KeyBytes()), string(op.ValueBytes()))
				if err != nil {
					return nil, err
				}
				resp.Responses = append(resp.Responses, &pb.ResponseOp{Response: &pb.ResponseOp_ResponsePut{
					ResponsePut: (*pb.PutResponse)(r),
				}})
			case op.IsDelete():
				r, err := fTxn.kv.Delete(ctx, string(op.KeyBytes()), clientv3.WithRange(string(op.RangeBytes())))
				if err != nil {
					return nil, err
				}
				resp.Responses = append(resp.Responses, &pb.ResponseOp{Response: &pb.ResponseOp_ResponseDeleteRange{
					ResponseDeleteRange: (*pb.DeleteRangeResponse)(r),
				}})
			case op.IsGet():
				// Get has no side effects but should return a response.
				r, err := fTxn.kv.Get(ctx, string(op.KeyBytes()), clientv3.WithRange(string(op.RangeBytes())))
				if err != nil {
					return nil, err
				}
				resp.Responses = append(resp.Responses, &pb.ResponseOp{Response: &pb.ResponseOp_ResponseRange{
					ResponseRange: (*pb.RangeResponse)(r),
				}})
			default:
				// other op types have no side effects in this fake.
			}
		}
	}

	return resp, nil
}

func (fTxn *fakeTxn) evalCmps(ctx context.Context) (bool, error) {
	for _, c := range fTxn.cmps {
		passed, err := fTxn.evalOneCmp(ctx, c)
		if err != nil {
			return false, err
		}
		if !passed {
			return false, nil
		}
	}
	return true, nil
}

func (fTxn *fakeTxn) evalOneCmp(ctx context.Context, c clientv3.Cmp) (bool, error) {
	pc, ok := toPBCompare(c)
	if !ok {
		return false, fmt.Errorf("unsupported cmp type %T", c)
	}
	// This fake only supports single-key comparisons.
	if len(pc.RangeEnd) != 0 {
		return false, fmt.Errorf("range comparisons are not supported in fake txn (key=%q)", string(pc.Key))
	}

	key := string(pc.Key)
	var curVal string
	exists := false
	if fTxn.kv != nil {
		gr, e := fTxn.kv.Get(ctx, key)
		if e != nil {
			return false, e
		}
		if len(gr.Kvs) > 0 {
			exists = true
			curVal = string(gr.Kvs[0].Value)
		}
	}

	cmpInt := func(a, b int64) (bool, error) {
		switch pc.Result {
		case pb.Compare_EQUAL:
			return a == b, nil
		case pb.Compare_NOT_EQUAL:
			return a != b, nil
		case pb.Compare_GREATER:
			return a > b, nil
		case pb.Compare_LESS:
			return a < b, nil
		default:
			return false, fmt.Errorf("unsupported compare result %v", pc.Result)
		}
	}
	cmpStr := func(a, b string) (bool, error) {
		switch pc.Result {
		case pb.Compare_EQUAL:
			return a == b, nil
		case pb.Compare_NOT_EQUAL:
			return a != b, nil
		case pb.Compare_GREATER:
			return a > b, nil
		case pb.Compare_LESS:
			return a < b, nil
		default:
			return false, fmt.Errorf("unsupported compare result %v", pc.Result)
		}
	}

	switch pc.Target {
	case pb.Compare_VALUE:
		tu, ok := pc.TargetUnion.(*pb.Compare_Value)
		if !ok {
			return false, fmt.Errorf("unexpected target union for VALUE: %T", pc.TargetUnion)
		}
		left := ""
		if exists {
			left = curVal
		}
		return cmpStr(left, string(tu.Value))

	case pb.Compare_VERSION:
		tu, ok := pc.TargetUnion.(*pb.Compare_Version)
		if !ok {
			return false, fmt.Errorf("unexpected target union for VERSION: %T", pc.TargetUnion)
		}
		// Fake: treat existence as version=1, non-existence as version=0.
		var left int64
		if exists {
			left = 1
		}
		return cmpInt(left, tu.Version)

	case pb.Compare_CREATE:
		tu, ok := pc.TargetUnion.(*pb.Compare_CreateRevision)
		if !ok {
			return false, fmt.Errorf("unexpected target union for CREATE: %T", pc.TargetUnion)
		}
		// Fake: treat existence as createRevision=1, non-existence as 0.
		var left int64
		if exists {
			left = 1
		}
		return cmpInt(left, tu.CreateRevision)

	case pb.Compare_MOD:
		tu, ok := pc.TargetUnion.(*pb.Compare_ModRevision)
		if !ok {
			return false, fmt.Errorf("unexpected target union for MOD: %T", pc.TargetUnion)
		}
		// Fake: treat existence as modRevision=1, non-existence as 0.
		var left int64
		if exists {
			left = 1
		}
		return cmpInt(left, tu.ModRevision)

	case pb.Compare_LEASE:
		tu, ok := pc.TargetUnion.(*pb.Compare_Lease)
		if !ok {
			return false, fmt.Errorf("unexpected target union for LEASE: %T", pc.TargetUnion)
		}
		// Lease isn't modeled in this fake; always treat lease as 0.
		return cmpInt(0, tu.Lease)

	default:
		return false, fmt.Errorf("unsupported compare target %v", pc.Target)
	}
}

func toPBCompare(c clientv3.Cmp) (pb.Compare, bool) {
	pbT := reflect.TypeOf(pb.Compare{})
	t := reflect.TypeOf(c)
	if t == nil {
		return pb.Compare{}, false
	}
	if t.ConvertibleTo(pbT) {
		v := reflect.ValueOf(c).Convert(pbT)
		return v.Interface().(pb.Compare), true
	}
	return pb.Compare{}, false
}

func NewEtcdLease() clientv3.Lease {
	return &fakeEtcdLease{}
}

type fakeEtcdLease struct{}

func (f *fakeEtcdLease) Grant(ctx context.Context, ttl int64) (*clientv3.LeaseGrantResponse, error) {
	return &clientv3.LeaseGrantResponse{ID: clientv3.LeaseID(rand.Int64()), TTL: ttl}, nil
}

func (f *fakeEtcdLease) Revoke(ctx context.Context, id clientv3.LeaseID) (*clientv3.LeaseRevokeResponse, error) {
	return &clientv3.LeaseRevokeResponse{}, nil
}

func (f *fakeEtcdLease) KeepAlive(ctx context.Context, id clientv3.LeaseID) (<-chan *clientv3.LeaseKeepAliveResponse, error) {
	return nil, nil
}

func (f *fakeEtcdLease) KeepAliveOnce(ctx context.Context, id clientv3.LeaseID) (*clientv3.LeaseKeepAliveResponse, error) {
	return &clientv3.LeaseKeepAliveResponse{}, nil
}

func (f *fakeEtcdLease) Leases(ctx context.Context) (*clientv3.LeaseLeasesResponse, error) {
	return &clientv3.LeaseLeasesResponse{}, nil
}

func (f *fakeEtcdLease) TimeToLive(ctx context.Context, id clientv3.LeaseID, opts ...clientv3.LeaseOption) (*clientv3.LeaseTimeToLiveResponse, error) {
	return &clientv3.LeaseTimeToLiveResponse{}, nil
}

func (f *fakeEtcdLease) Close() error {
	return nil
}
