package runtime

import (
	"context"
	"fmt"
	"log/slog"
	"sync"

	"github.com/hexahost/gamecloud/node-agent/internal/docker"
	"github.com/hexahost/gamecloud/node-agent/internal/protocol"
)

const (
	ServerStateProvisioning = "provisioning"
	ServerStateStopped      = "stopped"
	ServerStateStarting     = "starting"
	ServerStateRunning      = "running"
	ServerStateStopping     = "stopping"
	ServerStateDeleting     = "deleting"
	ServerStateDeleted      = "deleted"
	ServerStateError        = "error"

	ResultCodeOK    = "ok"
	ResultCodeError = "error"
)

// Responder sends protocol responses back to the control plane.
type Responder interface {
	SendState(ctx context.Context, correlationID string, payload protocol.ServerStatePayload) error
	SendOperationComplete(ctx context.Context, correlationID string, payload protocol.ServerOperationResultPayload) error
	SendOperationFailed(ctx context.Context, correlationID string, payload protocol.ServerOperationResultPayload) error
}

type serverRecord struct {
	ServerID    string
	ContainerID string
	HostPort    int
	DataPath    string
	RamMb       int
	State       string
}

// Manager tracks provisioned servers and executes lifecycle operations.
type Manager struct {
	mu      sync.RWMutex
	docker  *docker.Client
	servers map[string]*serverRecord
	resp    Responder
	log     *slog.Logger
}

// NewManager creates a runtime manager backed by the given Docker client.
func NewManager(dockerClient *docker.Client, resp Responder, log *slog.Logger) *Manager {
	return &Manager{
		docker:  dockerClient,
		servers: make(map[string]*serverRecord),
		resp:    resp,
		log:     log,
	}
}

// RunningCount returns the number of servers currently in the running state.
func (m *Manager) RunningCount() int {
	m.mu.RLock()
	defer m.mu.RUnlock()
	count := 0
	for _, rec := range m.servers {
		if rec.State == ServerStateRunning {
			count++
		}
	}
	return count
}

// HandleProvision creates a Minecraft container for the server.
func (m *Manager) HandleProvision(ctx context.Context, correlationID string, payload protocol.ServerProvisionPayload) {
	meta := payload.OperationMeta
	m.log.Info("provision server",
		"server_id", meta.ServerID,
		"host_port", payload.HostPort,
		"version", payload.MinecraftVersion,
	)

	if !payload.EulaAccepted {
		m.fail(ctx, correlationID, meta, "EULA_NOT_ACCEPTED", "EULA must be accepted before provisioning")
		return
	}

	m.mu.Lock()
	if existing, ok := m.servers[meta.ServerID]; ok {
		m.mu.Unlock()
		m.log.Info("provision idempotent hit", "server_id", meta.ServerID, "container_id", existing.ContainerID)
		m.complete(ctx, correlationID, meta, existing.State)
		return
	}
	m.mu.Unlock()

	m.emitState(ctx, correlationID, meta, ServerStateProvisioning, "", "")

	containerID, err := m.docker.CreateMinecraftServer(ctx, docker.MinecraftServerSpec{
		ServerID:         meta.ServerID,
		MinecraftVersion: payload.MinecraftVersion,
		SoftwareFamily:   payload.SoftwareFamily,
		HostPort:         payload.HostPort,
		RamMb:            payload.RamMb,
		DataPath:         payload.DataPath,
		EulaAccepted:     payload.EulaAccepted,
	})
	if err != nil {
		m.fail(ctx, correlationID, meta, "PROVISION_FAILED", err.Error())
		return
	}

	rec := &serverRecord{
		ServerID:    meta.ServerID,
		ContainerID: containerID,
		HostPort:    payload.HostPort,
		DataPath:    payload.DataPath,
		RamMb:       payload.RamMb,
		State:       ServerStateStopped,
	}

	m.mu.Lock()
	m.servers[meta.ServerID] = rec
	m.mu.Unlock()

	m.emitState(ctx, correlationID, meta, ServerStateStopped, "", "")
	m.complete(ctx, correlationID, meta, ServerStateStopped)
}

// HandleStart starts a provisioned server container.
func (m *Manager) HandleStart(ctx context.Context, correlationID string, payload protocol.ServerStartPayload) {
	meta := payload.OperationMeta
	rec, ok := m.getServer(meta.ServerID)
	if !ok {
		m.fail(ctx, correlationID, meta, "SERVER_NOT_FOUND", "server is not provisioned on this node")
		return
	}

	m.emitState(ctx, correlationID, meta, ServerStateStarting, "", "")

	if err := m.docker.StartContainer(ctx, rec.ContainerID); err != nil {
		m.setState(meta.ServerID, ServerStateError)
		m.emitState(ctx, correlationID, meta, ServerStateError, "START_FAILED", err.Error())
		m.fail(ctx, correlationID, meta, "START_FAILED", err.Error())
		return
	}

	m.setState(meta.ServerID, ServerStateRunning)
	m.emitState(ctx, correlationID, meta, ServerStateRunning, "", "")
	m.complete(ctx, correlationID, meta, ServerStateRunning)
}

// HandleStop stops a running server container.
func (m *Manager) HandleStop(ctx context.Context, correlationID string, payload protocol.ServerStopPayload) {
	meta := payload.OperationMeta
	rec, ok := m.getServer(meta.ServerID)
	if !ok {
		m.fail(ctx, correlationID, meta, "SERVER_NOT_FOUND", "server is not provisioned on this node")
		return
	}

	m.emitState(ctx, correlationID, meta, ServerStateStopping, "", "")

	if err := m.docker.StopContainer(ctx, rec.ContainerID); err != nil {
		m.setState(meta.ServerID, ServerStateError)
		m.emitState(ctx, correlationID, meta, ServerStateError, "STOP_FAILED", err.Error())
		m.fail(ctx, correlationID, meta, "STOP_FAILED", err.Error())
		return
	}

	m.setState(meta.ServerID, ServerStateStopped)
	m.emitState(ctx, correlationID, meta, ServerStateStopped, "", "")
	m.complete(ctx, correlationID, meta, ServerStateStopped)
}

// HandleDelete removes a server container and drops local state.
func (m *Manager) HandleDelete(ctx context.Context, correlationID string, meta protocol.OperationMeta) {
	rec, ok := m.getServer(meta.ServerID)
	if !ok {
		m.complete(ctx, correlationID, meta, ServerStateDeleted)
		return
	}

	m.emitState(ctx, correlationID, meta, ServerStateDeleting, "", "")

	if state, err := m.docker.GetContainerState(ctx, rec.ContainerID); err == nil && state.Running {
		if err := m.docker.StopContainer(ctx, rec.ContainerID); err != nil {
			m.fail(ctx, correlationID, meta, "DELETE_STOP_FAILED", err.Error())
			return
		}
	}

	if err := m.docker.RemoveContainer(ctx, rec.ContainerID); err != nil {
		m.fail(ctx, correlationID, meta, "DELETE_FAILED", err.Error())
		return
	}

	m.mu.Lock()
	delete(m.servers, meta.ServerID)
	m.mu.Unlock()

	m.emitState(ctx, correlationID, meta, ServerStateDeleted, "", "")
	m.complete(ctx, correlationID, meta, ServerStateDeleted)
}

func (m *Manager) getServer(serverID string) (*serverRecord, bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	rec, ok := m.servers[serverID]
	if !ok {
		return nil, false
	}
	copy := *rec
	return &copy, true
}

func (m *Manager) setState(serverID, state string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	if rec, ok := m.servers[serverID]; ok {
		rec.State = state
	}
}

func (m *Manager) emitState(ctx context.Context, correlationID string, meta protocol.OperationMeta, state, errorCode, errorMessage string) {
	payload := protocol.ServerStatePayload{
		OperationMeta: meta,
		State:         state,
		ErrorCode:     errorCode,
		ErrorMessage:  errorMessage,
	}
	if err := m.resp.SendState(ctx, correlationID, payload); err != nil {
		m.log.Warn("send server.state failed",
			"server_id", meta.ServerID,
			"state", state,
			"error", err,
		)
	}
}

func (m *Manager) complete(ctx context.Context, correlationID string, meta protocol.OperationMeta, _ string) {
	payload := protocol.ServerOperationResultPayload{
		OperationMeta: meta,
		ResultCode:    ResultCodeOK,
	}
	if err := m.resp.SendOperationComplete(ctx, correlationID, payload); err != nil {
		m.log.Warn("send operation complete failed",
			"server_id", meta.ServerID,
			"error", err,
		)
	}
}

func (m *Manager) fail(ctx context.Context, correlationID string, meta protocol.OperationMeta, errorCode, errorMessage string) {
	m.emitState(ctx, correlationID, meta, ServerStateError, errorCode, errorMessage)
	payload := protocol.ServerOperationResultPayload{
		OperationMeta: meta,
		ResultCode:    ResultCodeError,
		ErrorCode:     errorCode,
		ErrorMessage:  errorMessage,
	}
	if err := m.resp.SendOperationFailed(ctx, correlationID, payload); err != nil {
		m.log.Warn("send operation failed failed",
			"server_id", meta.ServerID,
			"error", err,
		)
	}
}
