package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"log"
	"strconv"
	"time"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/network"
	"github.com/docker/go-connections/nat"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

func (o *Orchestrator) ProvisioningLoop(ctx context.Context) {
	ticker := time.NewTicker(o.config.ProvisionInterval)
	defer ticker.Stop()

	log.Println("[provisioner] starting provisioning loop")
	for {
		select {
		case <-ctx.Done():
			log.Println("[provisioner] stopping provisioning loop")
			return
		case <-ticker.C:
			o.provisionPending(ctx)
		}
	}
}

func (o *Orchestrator) provisionPending(ctx context.Context) {
	var matches []Match
	// SKIP LOCKED so multiple orchestrator instances don't claim the same match.
	err := o.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).
		Where("status = ?", MatchStatusMatched).
		Find(&matches).Error
	if err != nil {
		log.Printf("[provisioner] error querying matched games: %v", err)
		return
	}

	for _, match := range matches {
		if err := o.provisionServer(ctx, match); err != nil {
			log.Printf("[provisioner] error provisioning server for match %s: %v", match.ID, err)
		}
	}
}

func (o *Orchestrator) provisionServer(ctx context.Context, match Match) error {
	log.Printf("[provisioner] provisioning server for match %s (map: %s)", match.ID, match.Map)

	port, err := o.allocatePort(ctx, match.ID)
	if err != nil {
		return fmt.Errorf("port allocation failed: %w", err)
	}

	// In demo mode the orchestrator spawns a lightweight stand-in container,
	// so no real Steam GSLT is required.
	gslt := "demo-token"
	if !o.config.DemoMode {
		gslt, err = o.allocateGSLT(match.ID)
		if err != nil {
			o.deallocatePort(ctx, port)
			return fmt.Errorf("GSLT allocation failed: %w", err)
		}
	}

	rconPassword := generateRCONPassword()

	containerName := fmt.Sprintf("tapper-cs2-%s", match.ID)
	var containerID string
	if o.config.DemoMode {
		containerID, err = o.createDemoContainer(ctx, match, port, containerName)
	} else {
		containerID, err = o.createContainer(ctx, match, port, gslt, rconPassword, containerName)
	}
	if err != nil {
		o.deallocatePort(ctx, port)
		if !o.config.DemoMode {
			o.deallocateGSLT(gslt)
		}
		return fmt.Errorf("container creation failed: %w", err)
	}

	if err := o.docker.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
		removeCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		_ = o.docker.ContainerRemove(removeCtx, containerID, container.RemoveOptions{Force: true})
		o.deallocatePort(ctx, port)
		if !o.config.DemoMode {
			o.deallocateGSLT(gslt)
		}
		return fmt.Errorf("container start failed: %w", err)
	}

	connectString := fmt.Sprintf("connect %s:%d", o.config.PublicHost, port)
	server := GameServer{
		MatchID:       &match.ID,
		ContainerID:   containerID,
		ContainerName: containerName,
		Host:          o.config.PublicHost,
		Port:          port,
		RCONPassword:  rconPassword,
		GSLTToken:     gslt,
		Status:        ServerStatusStarting,
		CreatedAt:     time.Now(),
	}

	err = o.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&server).Error; err != nil {
			return fmt.Errorf("failed to create game_server record: %w", err)
		}

		if err := tx.Model(&PortPool{}).
			Where("port = ?", port).
			Update("server_id", server.ID).Error; err != nil {
			return fmt.Errorf("failed to update port_pool server_id: %w", err)
		}

		if err := tx.Model(&Match{}).
			Where("id = ?", match.ID).
			Updates(map[string]interface{}{
				"status":         MatchStatusServerStarting,
				"server_id":      server.ID,
				"connect_string": connectString,
			}).Error; err != nil {
			return fmt.Errorf("failed to update match status: %w", err)
		}

		return nil
	})
	if err != nil {
		stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()
		stopTimeout := 10
		_ = o.docker.ContainerStop(stopCtx, containerID, container.StopOptions{Timeout: &stopTimeout})
		_ = o.docker.ContainerRemove(stopCtx, containerID, container.RemoveOptions{Force: true})
		o.deallocatePort(ctx, port)
		o.deallocateGSLT(gslt)
		return err
	}

	log.Printf("[provisioner] server provisioned for match %s: container=%s port=%d", match.ID, containerID[:12], port)
	return nil
}

func (o *Orchestrator) createContainer(ctx context.Context, match Match, port int, gslt, rconPassword, containerName string) (string, error) {
	portStr := strconv.Itoa(port)
	internalPort := "27015"

	env := []string{
		"SRCDS_TOKEN=" + gslt,
		"CS2_PORT=" + internalPort,
		"CS2_SERVERNAME=tapper Match " + match.ID[:8],
		"CS2_RCONPW=" + rconPassword,
		"CS2_PW=",
		"CS2_GAMEALIAS=competitive",
		"CS2_STARTMAP=" + match.Map,
		"CS2_MAXPLAYERS=12",
		"CS2_BOT_QUOTA=0",
		"CS2_SERVER_HIBERNATE=0",
		"CS2_LAN=0",
		"TV_ENABLE=1",
	}

	labels := map[string]string{
		"tapper":            "true",
		"tapper.managed":    "true",
		"tapper.match_id":   match.ID,
		"tapper.port":       portStr,
		"tapper.created_at": time.Now().UTC().Format(time.RFC3339),
	}

	tcpPort, _ := nat.NewPort("tcp", internalPort)
	udpPort, _ := nat.NewPort("udp", internalPort)

	exposedPorts := nat.PortSet{
		tcpPort: struct{}{},
		udpPort: struct{}{},
	}

	portBindings := nat.PortMap{
		tcpPort: []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: portStr}},
		udpPort: []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: portStr}},
	}

	// Attach to the gameservers network so CS2 can reach the API for webhooks.
	endpointsConfig := map[string]*network.EndpointSettings{}
	if o.config.GameserverNetwork != "" {
		endpointsConfig[o.config.GameserverNetwork] = &network.EndpointSettings{}
	}

	resp, err := o.docker.ContainerCreate(ctx,
		&container.Config{
			Image:        o.config.GameserverImage,
			Env:          env,
			Labels:       labels,
			ExposedPorts: exposedPorts,
		},
		&container.HostConfig{
			PortBindings: portBindings,
			RestartPolicy: container.RestartPolicy{
				Name: "no",
			},
			Resources: container.Resources{
				Memory:   4 * 1024 * 1024 * 1024,
				NanoCPUs: 2000000000,
			},
		},
		&network.NetworkingConfig{
			EndpointsConfig: endpointsConfig,
		},
		nil,
		containerName,
	)
	if err != nil {
		return "", fmt.Errorf("docker ContainerCreate: %w", err)
	}

	for _, warning := range resp.Warnings {
		log.Printf("[provisioner] docker warning for %s: %s", containerName, warning)
	}

	return resp.ID, nil
}

// createDemoContainer spawns a tiny alpine container in place of a real CS2
// dedicated server. Used in DEMO_MODE to show the orchestrator lifecycle on
// stage without needing to build the ~700MB gameserver image or hold a real
// Steam GSLT. The container just sleeps and prints heartbeats.
func (o *Orchestrator) createDemoContainer(ctx context.Context, match Match, port int, containerName string) (string, error) {
	portStr := strconv.Itoa(port)
	internalPort := "27015"

	tcpPort, _ := nat.NewPort("tcp", internalPort)
	exposedPorts := nat.PortSet{tcpPort: struct{}{}}
	portBindings := nat.PortMap{
		tcpPort: []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: portStr}},
	}

	labels := map[string]string{
		"tapper":            "true",
		"tapper.managed":    "true",
		"tapper.demo":       "true",
		"tapper.match_id":   match.ID,
		"tapper.port":       portStr,
		"tapper.created_at": time.Now().UTC().Format(time.RFC3339),
	}

	cmd := []string{
		"sh", "-c",
		fmt.Sprintf("echo '[demo cs2] match %s on %s, port %d'; while true; do echo '[demo cs2] alive'; sleep 15; done",
			match.ID[:8], match.Map, port),
	}

	resp, err := o.docker.ContainerCreate(ctx,
		&container.Config{
			Image:        "alpine:latest",
			Cmd:          cmd,
			Labels:       labels,
			ExposedPorts: exposedPorts,
		},
		&container.HostConfig{
			PortBindings:  portBindings,
			RestartPolicy: container.RestartPolicy{Name: "no"},
		},
		&network.NetworkingConfig{},
		nil,
		containerName,
	)
	if err != nil {
		return "", fmt.Errorf("docker ContainerCreate (demo): %w", err)
	}
	for _, w := range resp.Warnings {
		log.Printf("[provisioner] demo container warning for %s: %s", containerName, w)
	}
	return resp.ID, nil
}

func (o *Orchestrator) allocatePort(ctx context.Context, matchID string) (int, error) {
	var portEntry PortPool
	err := o.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		// SKIP LOCKED ensures no two provisioners grab the same port.
		if err := tx.Clauses(clause.Locking{
			Strength: "UPDATE",
			Options:  "SKIP LOCKED",
		}).
			Where("allocated = false").
			Order("port ASC").
			First(&portEntry).Error; err != nil {
			return fmt.Errorf("no available ports: %w", err)
		}

		portEntry.Allocated = true
		if err := tx.Save(&portEntry).Error; err != nil {
			return fmt.Errorf("failed to mark port as allocated: %w", err)
		}

		return nil
	})
	if err != nil {
		return 0, err
	}

	log.Printf("[provisioner] allocated port %d for match %s", portEntry.Port, matchID)
	return portEntry.Port, nil
}

func (o *Orchestrator) deallocatePort(ctx context.Context, port int) {
	err := o.db.WithContext(ctx).Model(&PortPool{}).
		Where("port = ?", port).
		Updates(map[string]interface{}{
			"allocated": false,
			"server_id": nil,
		}).Error
	if err != nil {
		log.Printf("[provisioner] error deallocating port %d: %v", port, err)
		return
	}
	log.Printf("[provisioner] deallocated port %d", port)
}

func (o *Orchestrator) allocateGSLT(matchID string) (string, error) {
	o.gsltMu.Lock()
	defer o.gsltMu.Unlock()

	for i, alloc := range o.gsltPool {
		if !alloc.Allocated {
			o.gsltPool[i].Allocated = true
			o.gsltPool[i].ServerID = matchID
			log.Printf("[provisioner] allocated GSLT token (index %d) for match %s", i, matchID)
			return alloc.Token, nil
		}
	}
	return "", fmt.Errorf("no available GSLT tokens (all %d in use)", len(o.gsltPool))
}

func (o *Orchestrator) deallocateGSLT(token string) {
	o.gsltMu.Lock()
	defer o.gsltMu.Unlock()

	for i, alloc := range o.gsltPool {
		if alloc.Token == token {
			o.gsltPool[i].Allocated = false
			o.gsltPool[i].ServerID = ""
			log.Printf("[provisioner] deallocated GSLT token (index %d)", i)
			return
		}
	}
	log.Printf("[provisioner] warning: attempted to deallocate unknown GSLT token")
}

func generateRCONPassword() string {
	bytes := make([]byte, 16)
	if _, err := rand.Read(bytes); err != nil {
		return fmt.Sprintf("rcon_%d", time.Now().UnixNano())
	}
	return hex.EncodeToString(bytes)
}

func (o *Orchestrator) SeedPortPool() error {
	for port := o.config.PortRangeStart; port <= o.config.PortRangeEnd; port++ {
		entry := PortPool{Port: port, Allocated: false}
		if err := o.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&entry).Error; err != nil {
			return fmt.Errorf("failed to seed port %d: %w", port, err)
		}
	}
	log.Printf("[provisioner] port pool seeded: %d-%d (%d ports)",
		o.config.PortRangeStart, o.config.PortRangeEnd,
		o.config.PortRangeEnd-o.config.PortRangeStart+1)
	return nil
}
