package main

import (
	"context"
	"fmt"
	"log"
	"time"

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

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

	log.Println("[monitor] starting health check loop")
	for {
		select {
		case <-ctx.Done():
			log.Println("[monitor] stopping health check loop")
			return
		case <-ticker.C:
			o.checkStartingServers(ctx)
		}
	}
}

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

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

func (o *Orchestrator) checkServerHealth(ctx context.Context, server GameServer) {
	age := time.Since(server.CreatedAt)

	running, err := o.isContainerRunning(ctx, server.ContainerID)
	if err != nil {
		log.Printf("[monitor] error checking container %s: %v", server.ContainerID[:12], err)
		return
	}

	if !running {
		log.Printf("[monitor] container %s for match %s is not running (crashed during boot)", server.ContainerID[:12], *server.MatchID)
		o.handleServerCrash(ctx, server)
		return
	}

	if age > o.config.MaxBootWait {
		log.Printf("[monitor] server for match %s exceeded max boot wait (%v)", *server.MatchID, o.config.MaxBootWait)
		o.handleBootTimeout(ctx, server)
		return
	}

	if !o.config.DemoMode {
		err = o.tryRCONConnection(server)
		if err != nil {
			// Suppress noise during the first 10s while CS2 is still booting.
			if age > 10*time.Second {
				log.Printf("[monitor] server %s not ready yet (age: %v): %v", server.ContainerID[:12], age.Round(time.Second), err)
			}
			return
		}

		log.Printf("[monitor] server %s is ready after %v, loading match config", server.ContainerID[:12], age.Round(time.Second))
		if err := o.loadMatchConfig(server); err != nil {
			// Retried on the next tick.
			log.Printf("[monitor] failed to load match config for %s: %v", *server.MatchID, err)
			return
		}
	} else {
		// DEMO_MODE: alpine container has no RCON; treat "running" as "ready".
		log.Printf("[monitor] demo server %s ready after %v", server.ContainerID[:12], age.Round(time.Second))
	}

	now := time.Now()
	if err := o.db.WithContext(ctx).Model(&GameServer{}).
		Where("id = ?", server.ID).
		Update("status", ServerStatusReady).Error; err != nil {
		log.Printf("[monitor] failed to update server %s status: %v", server.ID, err)
		return
	}

	if err := o.db.WithContext(ctx).Model(&Match{}).
		Where("id = ?", *server.MatchID).
		Updates(map[string]interface{}{
			"status":     MatchStatusWaitingForPlayers,
			"started_at": now,
		}).Error; err != nil {
		log.Printf("[monitor] failed to update match %s status: %v", *server.MatchID, err)
		return
	}

	log.Printf("[monitor] match %s transitioned to WAITING_FOR_PLAYERS", *server.MatchID)
}

func (o *Orchestrator) handleBootTimeout(ctx context.Context, server GameServer) {
	log.Printf("[monitor] aborting match %s due to boot timeout", *server.MatchID)

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

	o.db.WithContext(ctx).Model(&Match{}).
		Where("id = ?", *server.MatchID).
		Update("status", MatchStatusAborted)

	go o.cleanupServer(context.Background(), server)
}

func (o *Orchestrator) handleServerCrash(ctx context.Context, server GameServer) {
	matchID := ""
	if server.MatchID != nil {
		matchID = *server.MatchID
	}
	log.Printf("[monitor] handling crash for server %s (match: %s)", server.ID, matchID)

	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 {
			log.Printf("[monitor] error looking up match %s: %v", *server.MatchID, err)
		} else {
			newStatus := MatchStatusAborted
			if match.Status == MatchStatusLive {
				newStatus = MatchStatusCancelled
			}
			o.db.WithContext(ctx).Model(&Match{}).
				Where("id = ?", match.ID).
				Update("status", newStatus)
			log.Printf("[monitor] match %s transitioned to %s due to server crash", match.ID, newStatus)
		}
	}

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

	removeCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	_ = o.docker.ContainerRemove(removeCtx, server.ContainerID, container.RemoveOptions{Force: true})
}

func (o *Orchestrator) MonitorContainerEvents(ctx context.Context) {
	log.Println("[monitor] starting container event monitor")

	filterArgs := filters.NewArgs()
	filterArgs.Add("label", "tapper.managed=true")
	filterArgs.Add("type", "container")
	filterArgs.Add("event", "die")
	filterArgs.Add("event", "stop")
	filterArgs.Add("event", "kill")
	filterArgs.Add("event", "oom")

	eventCh, errCh := o.docker.Events(ctx, events.ListOptions{
		Filters: filterArgs,
	})

	for {
		select {
		case <-ctx.Done():
			log.Println("[monitor] stopping container event monitor")
			return
		case err := <-errCh:
			if err != nil {
				log.Printf("[monitor] docker events error: %v", err)
				time.Sleep(5 * time.Second)
				eventCh, errCh = o.docker.Events(ctx, events.ListOptions{
					Filters: filterArgs,
				})
			}
		case event := <-eventCh:
			matchID := event.Actor.Attributes["tapper.match_id"]
			containerID := event.Actor.ID
			log.Printf("[monitor] container event: %s for container %s (match: %s)",
				event.Action, containerID[:12], matchID)

			var server GameServer
			if err := o.db.Where("container_id = ? AND status NOT IN ?",
				containerID, []string{ServerStatusStopped, ServerStatusStopping}).
				First(&server).Error; err != nil {
				continue
			}

			o.handleServerCrash(context.Background(), server)
		}
	}
}

func (o *Orchestrator) isContainerRunning(ctx context.Context, containerID string) (bool, error) {
	inspect, err := o.docker.ContainerInspect(ctx, containerID)
	if err != nil {
		return false, fmt.Errorf("container inspect: %w", err)
	}
	return inspect.State.Running, nil
}

// LiveServerMonitorLoop force-stops servers whose match has exceeded the
// configured max duration, as a safety valve for stuck matches.
func (o *Orchestrator) LiveServerMonitorLoop(ctx context.Context) {
	ticker := time.NewTicker(o.config.CleanupInterval)
	defer ticker.Stop()

	log.Println("[monitor] starting live server monitor loop")
	for {
		select {
		case <-ctx.Done():
			log.Println("[monitor] stopping live server monitor loop")
			return
		case <-ticker.C:
			o.checkExpiredServers(ctx)
		}
	}
}

func (o *Orchestrator) checkExpiredServers(ctx context.Context) {
	cutoff := time.Now().Add(-o.config.MaxMatchDuration)

	var servers []GameServer
	if err := o.db.WithContext(ctx).
		Where("status IN ? AND created_at < ?",
			[]string{ServerStatusReady, ServerStatusLive}, cutoff).
		Find(&servers).Error; err != nil {
		log.Printf("[monitor] error querying expired servers: %v", err)
		return
	}

	for _, server := range servers {
		log.Printf("[monitor] force-stopping expired server %s (age exceeds %v)", server.ID, o.config.MaxMatchDuration)
		go o.cleanupServer(context.Background(), server)
	}
}
