package app

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

	"git.netflux.io/rob/octoplex/config"
	"git.netflux.io/rob/octoplex/container"
	"git.netflux.io/rob/octoplex/domain"
	"git.netflux.io/rob/octoplex/mediaserver"
	"git.netflux.io/rob/octoplex/multiplexer"
	"git.netflux.io/rob/octoplex/terminal"
)

// RunParams holds the parameters for running the application.
type RunParams struct {
	Config             config.Config
	DockerClient       container.DockerClient
	ClipboardAvailable bool
	BuildInfo          domain.BuildInfo
	Logger             *slog.Logger
}

// Run starts the application, and blocks until it exits.
func Run(ctx context.Context, params RunParams) error {
	state := newStateFromRunParams(params)
	logger := params.Logger

	ui, err := terminal.StartActor(ctx, terminal.StartActorParams{
		ClipboardAvailable: params.ClipboardAvailable,
		BuildInfo:          params.BuildInfo,
		Logger:             logger.With("component", "ui"),
	})
	if err != nil {
		return fmt.Errorf("start terminal user interface: %w", err)
	}
	defer ui.Close()

	updateUI := func() { ui.SetState(*state) }
	updateUI()

	containerClient, err := container.NewClient(ctx, params.DockerClient, logger.With("component", "container_client"))
	if err != nil {
		return fmt.Errorf("new container client: %w", err)
	}
	defer containerClient.Close()

	if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
		return fmt.Errorf("check existing containers: %w", err)
	} else if exists {
		if <-ui.ShowStartupCheckModal() {
			if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
				return fmt.Errorf("remove existing containers: %w", err)
			}
		} else {
			return nil
		}
	}

	srv := mediaserver.StartActor(ctx, mediaserver.StartActorParams{
		ContainerClient: containerClient,
		Logger:          logger.With("component", "mediaserver"),
	})
	defer srv.Close()

	mp := multiplexer.NewActor(ctx, multiplexer.NewActorParams{
		SourceURL:       srv.State().RTMPInternalURL,
		ContainerClient: containerClient,
		Logger:          logger.With("component", "multiplexer"),
	})
	defer mp.Close()

	const uiUpdateInterval = 2 * time.Second
	uiUpdateT := time.NewTicker(uiUpdateInterval)
	defer uiUpdateT.Stop()

	for {
		select {
		case cmd, ok := <-ui.C():
			if !ok {
				// TODO: keep UI open until all containers have closed
				logger.Info("UI closed")
				return nil
			}

			logger.Info("Command received", "cmd", cmd.Name())
			switch c := cmd.(type) {
			case terminal.CommandToggleDestination:
				mp.ToggleDestination(c.URL)
			case terminal.CommandQuit:
				return nil
			}
		case <-uiUpdateT.C:
			updateUI()
		case serverState := <-srv.C():
			applyServerState(serverState, state)
			updateUI()
		case mpState := <-mp.C():
			applyMultiplexerState(mpState, state)
			updateUI()
		}
	}
}

// applyServerState applies the current server state to the app state.
func applyServerState(serverState domain.Source, appState *domain.AppState) {
	appState.Source = serverState
}

// applyMultiplexerState applies the current multiplexer state to the app state.
func applyMultiplexerState(mpState multiplexer.State, appState *domain.AppState) {
	for i, dest := range appState.Destinations {
		if dest.URL != mpState.URL {
			continue
		}

		appState.Destinations[i].Container = mpState.Container
		appState.Destinations[i].Status = mpState.Status

		break
	}
}

// newStateFromRunParams creates a new app state from the run parameters.
func newStateFromRunParams(params RunParams) *domain.AppState {
	var state domain.AppState

	state.Destinations = make([]domain.Destination, 0, len(params.Config.Destinations))
	for _, dest := range params.Config.Destinations {
		state.Destinations = append(state.Destinations, domain.Destination{
			Name: dest.Name,
			URL:  dest.URL,
		})
	}

	return &state
}