package testutils

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sync"

	"github.com/google/go-cmp/cmp"
	"github.com/tidbcloud/aws-shared-provider/pkg/tidbworker/resourcemanager/poolmanager/factory"
	rm "github.com/tidbcloud/aws-shared-provider/pkg/tidbworker/resourcemanager/types"
	"github.com/tidbcloud/aws-shared-provider/pkg/utils/tidb"
)

type mockTiDBPools struct {
	sync.RWMutex
	cfg       *ClusterConfig
	instances map[string]*mockTiDB
}

func newMockTiDBPools(cfg *ClusterConfig) *mockTiDBPools {
	instances := make(map[string]*mockTiDB)
	for pool, pc := range cfg.PoolsConfig.Pools {
		for i := 0; i < pc.Size; i++ {
			key := factory.WorkerStatusEndpoint(factory.WorkerHandle(pool, i), cfg.Namespace)
			instances[key] = &mockTiDB{pool: pool, index: i}
		}
	}
	return &mockTiDBPools{
		cfg:       cfg.Clone(),
		instances: instances,
	}
}

func (m *mockTiDBPools) updateCluster(cfg *ClusterConfig) {
	m.Lock()
	defer m.Unlock()
	for pool, pc := range cfg.PoolsConfig.Pools {
		for i := 0; i < pc.Size; i++ {
			key := factory.WorkerStatusEndpoint(factory.WorkerHandle(pool, i), cfg.Namespace)
			if m.instances[key] == nil {
				m.instances[key] = &mockTiDB{pool: pool, index: i}
			}
		}
	}
	for key, instance := range m.instances {
		if pc, ok := cfg.PoolsConfig.Pools[instance.pool]; !ok || instance.index >= pc.Size {
			delete(m.instances, key)
		}
	}
	m.cfg = cfg.Clone()
}

func (m *mockTiDBPools) doRequest(r *http.Request) (*http.Response, error) {
	m.RLock()
	instance, ok := m.instances[r.Host]
	m.RUnlock()
	if !ok {
		return nil, fmt.Errorf("tidb instance %s not found", r.Host)
	}
	return instance.handleRequest(r), nil
}

func (m *mockTiDBPools) getStandbyWorkers(pool rm.WorkerPool) []rm.Worker {
	m.RLock()
	defer m.RUnlock()
	var workers []rm.Worker
	for _, instance := range m.instances {
		if instance.pool == pool && instance.isStandby() {
			workers = append(workers, factory.WorkerHandle(pool, instance.index))
		}
	}
	return workers
}

func (m *mockTiDBPools) activate(worker rm.Worker, parameter tidb.ActivateRequest) error {
	m.RLock()
	key := factory.WorkerStatusEndpoint(worker, m.cfg.Namespace)
	instance := m.instances[key]
	m.RUnlock()
	if instance == nil {
		return fmt.Errorf("tidb instance %s not found", key)
	}
	instance.setActivated(&parameter)
	return nil
}

func (m *mockTiDBPools) recycle(worker rm.Worker) error {
	m.RLock()
	key := factory.WorkerStatusEndpoint(worker, m.cfg.Namespace)
	instance := m.instances[key]
	m.RUnlock()
	if instance == nil {
		return fmt.Errorf("tidb instance %s not found", key)
	}
	instance.setActivated(nil)
	return nil
}

type mockTiDB struct {
	pool  rm.WorkerPool
	index int
	sync.Mutex
	activated       bool
	activateRequest *tidb.ActivateRequest
}

func (t *mockTiDB) handleRequest(r *http.Request) *http.Response {
	switch fmt.Sprintf("%s %s", r.Method, r.URL.Path) {
	case "POST /tidb-pool/activate":
		return t.activate(r)
	case "POST /tidb-pool/exit":
		return t.recycle(r)
	case "GET /tidb-pool/status":
		return t.status(r)
	default:
		return &http.Response{
			StatusCode: http.StatusNotFound,
			Body:       nil,
		}
	}
}

func (t *mockTiDB) activate(r *http.Request) *http.Response {
	activateRequest := &tidb.ActivateRequest{}
	data, err := io.ReadAll(r.Body)
	defer r.Body.Close() //nolint: errcheck // test
	if err != nil {
		return &http.Response{
			StatusCode: http.StatusBadRequest,
		}
	}
	if err = json.Unmarshal(data, activateRequest); err != nil {
		return &http.Response{
			StatusCode: http.StatusBadRequest,
		}
	}
	t.Lock()
	defer t.Unlock()
	if t.activated && !cmp.Equal(t.activateRequest, activateRequest) {
		return &http.Response{
			StatusCode: http.StatusPreconditionFailed,
		}
	}
	t.activated = true
	t.activateRequest = activateRequest
	return &http.Response{
		StatusCode: http.StatusOK,
	}
}

func (t *mockTiDB) recycle(_ *http.Request) *http.Response {
	t.Lock()
	defer t.Unlock()
	t.activated = false
	t.activateRequest = nil
	return &http.Response{
		StatusCode: http.StatusOK,
	}
}

func (t *mockTiDB) status(_ *http.Request) *http.Response {
	t.Lock()
	defer t.Unlock()
	var status struct {
		State string `json:"state"`
	}
	if t.activated {
		status.State = "activated"
	} else {
		status.State = "standby"
	}
	data, _ := json.Marshal(status)
	return &http.Response{
		StatusCode: http.StatusOK,
		Header:     http.Header{"Content-Type": []string{"application/json"}},
		Body:       io.NopCloser(bytes.NewBuffer(data)),
	}
}

func (t *mockTiDB) isStandby() bool {
	t.Lock()
	defer t.Unlock()
	return !t.activated
}

func (t *mockTiDB) setActivated(request *tidb.ActivateRequest) {
	t.Lock()
	defer t.Unlock()
	t.activated = request != nil
	t.activateRequest = request
}
