package main

import (
	"context"
	"log"
	"time"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/filters"
)

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

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

func (o *Orchestrator) cleanupTerminalMatches(ctx context.Context) {
	var servers []GameServer
	err := o.db.WithContext(ctx).
		Joins("JOIN matches ON matches.server_id = game_servers.id").
		Where("matches.status IN ? AND game_servers.status NOT IN ?",
			[]string{MatchStatusFinished, MatchStatusAborted, MatchStatusCancelled, MatchStatusCleanup},
			[]string{ServerStatusStopping, ServerStatusStopped}).
		Find(&servers).Error
	if err != nil {
		log.Printf("[cleanup] error querying terminal match servers: %v", err)
		return
	}

	for _, server := range servers {
		log.Printf("[cleanup] cleaning up server %s for terminal match %s", server.ID, safeMatchID(server.MatchID))
		go o.cleanupServer(ctx, server)
	}
}

func (o *Orchestrator) cleanupStoppingServers(ctx context.Context) {
	var servers []GameServer
	if err := o.db.WithContext(ctx).
		Where("status = ?", ServerStatusStopping).
		Find(&servers).Error; err != nil {
		log.Printf("[cleanup] error querying stopping servers: %v", err)
		return
	}

	for _, server := range servers {
		o.finalizeServerRemoval(ctx, server)
	}
}

func (o *Orchestrator) cleanupServer(ctx context.Context, server GameServer) {
	log.Printf("[cleanup] cleaning up server %s (container: %s, port: %d)",
		server.ID, server.ContainerID[:min(12, len(server.ContainerID))], server.Port)

	o.db.WithContext(ctx).Model(&GameServer{}).
		Where("id = ?", server.ID).
		Update("status", ServerStatusStopping)

	stopCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	stopTimeout := 15
	if err := o.docker.ContainerStop(stopCtx, server.ContainerID, container.StopOptions{Timeout: &stopTimeout}); err != nil {
		log.Printf("[cleanup] error stopping container %s: %v (may already be stopped)", server.ContainerID[:12], err)
	}

	o.finalizeServerRemoval(ctx, server)
}

func (o *Orchestrator) finalizeServerRemoval(ctx context.Context, server GameServer) {
	removeCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()

	err := o.docker.ContainerRemove(removeCtx, server.ContainerID, container.RemoveOptions{
		Force:         true,
		RemoveVolumes: true,
	})
	if err != nil {
		log.Printf("[cleanup] error removing container %s: %v", server.ContainerID[:12], err)
	}

	now := time.Now()

	o.db.WithContext(ctx).Model(&GameServer{}).
		Where("id = ?", server.ID).
		Updates(map[string]interface{}{
			"status":     ServerStatusStopped,
			"stopped_at": now,
		})

	if server.MatchID != nil {
		o.db.WithContext(ctx).Model(&Match{}).
			Where("id = ? AND status IN ?", *server.MatchID,
				[]string{MatchStatusFinished, MatchStatusAborted, MatchStatusCancelled}).
			Update("status", MatchStatusCleanup)
	}

	o.deallocatePort(ctx, server.Port)
	o.deallocateGSLT(server.GSLTToken)

	log.Printf("[cleanup] server %s fully cleaned up", server.ID)
}

// ReconcileOnStartup recovers from orphaned containers and stale DB records
// left behind by an unclean shutdown.
func (o *Orchestrator) ReconcileOnStartup(ctx context.Context) error {
	log.Println("[cleanup] starting orphan recovery / reconciliation")

	filterArgs := filters.NewArgs()
	filterArgs.Add("label", "tapper.managed=true")

	containers, err := o.docker.ContainerList(ctx, container.ListOptions{
		All:     true,
		Filters: filterArgs,
	})
	if err != nil {
		return err
	}

	log.Printf("[cleanup] found %d tapper containers on host", len(containers))

	dockerContainers := make(map[string]container.Summary)
	for _, c := range containers {
		dockerContainers[c.ID] = c
	}

	var dbServers []GameServer
	if err := o.db.WithContext(ctx).
		Where("status NOT IN ?", []string{ServerStatusStopped}).
		Find(&dbServers).Error; err != nil {
		return err
	}

	for _, server := range dbServers {
		if _, exists := dockerContainers[server.ContainerID]; !exists {
			log.Printf("[cleanup] orphaned DB record: server %s (container %s) - container not found, marking stopped",
				server.ID, server.ContainerID[:min(12, len(server.ContainerID))])

			now := time.Now()
			o.db.WithContext(ctx).Model(&GameServer{}).
				Where("id = ?", server.ID).
				Updates(map[string]interface{}{
					"status":     ServerStatusStopped,
					"stopped_at": now,
				})

			if server.MatchID != nil {
				var match Match
				if err := o.db.WithContext(ctx).Where("id = ?", *server.MatchID).First(&match).Error; err == nil {
					if match.Status != MatchStatusFinished && match.Status != MatchStatusCleanup {
						o.db.WithContext(ctx).Model(&Match{}).
							Where("id = ?", match.ID).
							Update("status", MatchStatusCancelled)
					}
				}
			}

			o.deallocatePort(ctx, server.Port)
			o.deallocateGSLT(server.GSLTToken)

			delete(dockerContainers, server.ContainerID)
		} else {
			delete(dockerContainers, server.ContainerID)
		}
	}

	// Anything left in dockerContainers is an orphan container not in the DB.
	for containerID, c := range dockerContainers {
		matchID := c.Labels["tapper.match_id"]
		log.Printf("[cleanup] orphaned container: %s (match: %s) - not tracked in DB, removing",
			containerID[:12], matchID)

		stopTimeout := 10
		_ = o.docker.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &stopTimeout})
		_ = o.docker.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true, RemoveVolumes: true})
	}

	o.db.WithContext(ctx).Exec(`
		UPDATE port_pool SET allocated = false, server_id = NULL
		WHERE allocated = true AND (
			server_id IS NULL OR
			server_id NOT IN (SELECT id FROM game_servers WHERE status NOT IN ('stopped'))
		)
	`)

	o.rebuildGSLTAllocations(ctx)

	log.Println("[cleanup] reconciliation complete")
	return nil
}

func (o *Orchestrator) rebuildGSLTAllocations(ctx context.Context) {
	o.gsltMu.Lock()
	defer o.gsltMu.Unlock()

	for i := range o.gsltPool {
		o.gsltPool[i].Allocated = false
		o.gsltPool[i].ServerID = ""
	}

	var activeServers []GameServer
	o.db.WithContext(ctx).
		Where("status NOT IN ?", []string{ServerStatusStopped}).
		Find(&activeServers)

	for _, server := range activeServers {
		for i, alloc := range o.gsltPool {
			if alloc.Token == server.GSLTToken {
				o.gsltPool[i].Allocated = true
				if server.MatchID != nil {
					o.gsltPool[i].ServerID = *server.MatchID
				}
				break
			}
		}
	}

	allocated := 0
	for _, a := range o.gsltPool {
		if a.Allocated {
			allocated++
		}
	}
	log.Printf("[cleanup] GSLT pool rebuilt: %d/%d tokens allocated", allocated, len(o.gsltPool))
}

func safeMatchID(id *string) string {
	if id != nil {
		return *id
	}
	return "unknown"
}
