package daemon

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/daemoncontrol"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

const (
	windowsLegacyTaskTotalTimeout      = 20 * time.Second
	windowsLegacyTaskObservationWindow = 10 * time.Second
	windowsLegacyProcessExitWindow     = 5 * time.Second
	windowsLegacyTaskPollInterval      = 2 * time.Second
	windowsLegacyProcessPollInterval   = time.Second
	windowsPowerShellQueryTimeout      = 10 * time.Second
)

type windowsTaskSnapshot struct {
	Exists      bool   `json:"exists"`
	TaskPath    string `json:"taskPath"`
	TaskName    string `json:"taskName"`
	State       string `json:"state"`
	ActionCount int    `json:"actionCount"`
	Execute     string `json:"execute"`
	Arguments   string `json:"arguments"`
}

type windowsProcessSnapshot struct {
	Complete       bool  `json:"complete"`
	ProcessIDs     []int `json:"processIds"`
	UnreadablePIDs []int `json:"unreadablePids"`
}

var windowsTaskSnapshotQuery = queryWindowsAgentTeamTask
var windowsProcessSnapshotQuery = queryWindowsAgentTeamProcesses
var windowsTaskSleep = sleepWindowsTaskContext

const windowsLoopTaskSnapshotScript = `$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$tasks = @(Get-ScheduledTask -TaskPath '\' -ErrorAction Stop | Where-Object { $_.TaskName -eq 'AgentTeam' })
if ($tasks.Count -eq 0) {
    [pscustomobject]@{ exists = $false } | ConvertTo-Json -Compress
    exit 0
}
if ($tasks.Count -ne 1) {
    throw "Expected one root AgentTeam scheduled task, found $($tasks.Count)"
}
$task = $tasks[0]
$actions = @($task.Actions)
$execute = ''
$arguments = ''
if ($actions.Count -eq 1) {
    $execute = [string]$actions[0].Execute
    $arguments = [string]$actions[0].Arguments
}
[pscustomobject]@{
    exists = $true
    taskPath = [string]$task.TaskPath
    taskName = [string]$task.TaskName
    state = [string]$task.State
    actionCount = [int]$actions.Count
    execute = $execute
    arguments = $arguments
} | ConvertTo-Json -Compress`

const windowsAgentTeamProcessSnapshotScript = `$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$target = $env:AGENTTEAM_LEGACY_PROCESS_PATH
if ([string]::IsNullOrWhiteSpace($target)) {
    throw 'AGENTTEAM_LEGACY_PROCESS_PATH is required'
}
$target = [System.IO.Path]::GetFullPath($target)
$matches = @()
$unreadable = @()
$processes = @(Get-CimInstance Win32_Process -Filter "Name = 'agentteam.exe'" -ErrorAction Stop)
foreach ($process in $processes) {
    if ([string]::IsNullOrWhiteSpace($process.ExecutablePath)) {
        $unreadable += [int]$process.ProcessId
        continue
    }
    try {
        $processPath = [System.IO.Path]::GetFullPath($process.ExecutablePath)
    } catch {
        $unreadable += [int]$process.ProcessId
        continue
    }
    if ([string]::Equals($processPath, $target, [System.StringComparison]::OrdinalIgnoreCase)) {
        $matches += [int]$process.ProcessId
    }
}
[pscustomobject]@{
    complete = ($unreadable.Count -eq 0)
    processIds = @($matches)
    unreadablePids = @($unreadable)
} | ConvertTo-Json -Compress`

// This bridge can be removed when upgrades from receipt-less Wails builds are
// no longer supported.
func reconcileLegacyWindowsService(owner daemoncontrol.OwnerKind) error {
	if runtime.GOOS != "windows" || owner != daemoncontrol.OwnerKindElectron {
		return nil
	}
	managedBinary := filepath.Join(profile.ConfigDir(), "bin", "agentteam.exe")
	return newWindowsInstaller(managedBinary, profile.Path()).reconcileLegacyElectronTask()
}

func (w *windowsInstaller) reconcileLegacyElectronTask() error {
	ctx, cancel := context.WithTimeout(context.Background(), windowsLegacyTaskTotalTimeout)
	defer cancel()

	observationStarted := time.Now()
	task, err := windowsTaskSnapshotQuery(ctx)
	if err != nil {
		return err
	}
	if !w.matchesLegacyTask(task) {
		return nil
	}
	if !strings.EqualFold(strings.TrimSpace(task.State), "Running") {
		return w.waitForManagedProcessExit(ctx)
	}

	for elapsed := time.Since(observationStarted); elapsed < windowsLegacyTaskObservationWindow; {
		delay := min(windowsLegacyTaskPollInterval, windowsLegacyTaskObservationWindow-elapsed)
		if err := windowsTaskSleep(ctx, delay); err != nil {
			return fmt.Errorf("observe scheduled task %q before cleanup: %w", w.taskName, err)
		}
		elapsed += delay
		queryStarted := time.Now()
		task, err = windowsTaskSnapshotQuery(ctx)
		elapsed += time.Since(queryStarted)
		if err != nil {
			return fmt.Errorf("observe scheduled task %q before cleanup: %w", w.taskName, err)
		}
		if !task.Exists || !strings.EqualFold(strings.TrimSpace(task.State), "Running") {
			return w.waitForManagedProcessExit(ctx)
		}
		if !w.matchesLegacyTask(task) {
			return fmt.Errorf("scheduled task %q changed while waiting for the current daemon lock", w.taskName)
		}
	}

	taskTarget := `\` + w.taskName
	if out, err := windowsTaskCommandOutput(ctx, "/End", "/TN", taskTarget); err != nil {
		return fmt.Errorf("stop legacy scheduled task %q: %w: %s", w.taskName, err, strings.TrimSpace(string(out)))
	}
	task, err = w.waitForStoppedLegacyTask(ctx)
	if err != nil {
		return err
	}
	if !task.Exists {
		return nil
	}

	if out, err := windowsTaskCommandOutput(ctx, "/Delete", "/TN", taskTarget, "/F"); err != nil {
		return fmt.Errorf(
			"delete legacy scheduled task %q: %w: %s; run an elevated Command Prompt, execute `schtasks /Delete /TN %s /F`, then retry AgentTeam",
			w.taskName,
			err,
			strings.TrimSpace(string(out)),
			taskTarget,
		)
	}
	task, err = windowsTaskSnapshotQuery(ctx)
	if err != nil {
		return fmt.Errorf("verify legacy scheduled task %q deleted: %w", w.taskName, err)
	}
	if task.Exists {
		return fmt.Errorf("legacy scheduled task %q still exists after deletion", w.taskName)
	}
	return w.waitForManagedProcessExit(ctx)
}

func (w *windowsInstaller) waitForStoppedLegacyTask(ctx context.Context) (windowsTaskSnapshot, error) {
	for elapsed := time.Duration(0); ; elapsed += windowsLegacyProcessPollInterval {
		task, err := windowsTaskSnapshotQuery(ctx)
		if err != nil {
			return windowsTaskSnapshot{}, fmt.Errorf("verify legacy scheduled task %q stopped: %w", w.taskName, err)
		}
		if task.Exists && !w.matchesLegacyTask(task) {
			return windowsTaskSnapshot{}, fmt.Errorf("scheduled task %q changed before legacy registration deletion", w.taskName)
		}
		processes, err := windowsProcessSnapshotQuery(ctx, w.binPath)
		if err != nil {
			return windowsTaskSnapshot{}, fmt.Errorf("verify managed process exit for scheduled task %q: %w", w.taskName, err)
		}
		if (!task.Exists || !strings.EqualFold(strings.TrimSpace(task.State), "Running")) && processes.Complete && len(processes.ProcessIDs) == 0 {
			return task, nil
		}
		if elapsed >= windowsLegacyProcessExitWindow {
			return windowsTaskSnapshot{}, fmt.Errorf(
				"legacy scheduled task %q did not settle within %s (exists: %t, state: %q, process query complete: %t, processes: %v, unreadable processes: %v)",
				w.taskName,
				windowsLegacyProcessExitWindow,
				task.Exists,
				task.State,
				processes.Complete,
				processes.ProcessIDs,
				processes.UnreadablePIDs,
			)
		}
		if err := windowsTaskSleep(ctx, windowsLegacyProcessPollInterval); err != nil {
			return windowsTaskSnapshot{}, fmt.Errorf("wait for legacy scheduled task %q to stop: %w", w.taskName, err)
		}
	}
}

func (w *windowsInstaller) waitForManagedProcessExit(ctx context.Context) error {
	for elapsed := time.Duration(0); ; elapsed += windowsLegacyProcessPollInterval {
		processes, err := windowsProcessSnapshotQuery(ctx, w.binPath)
		if err != nil {
			return fmt.Errorf("verify managed process exit for scheduled task %q: %w", w.taskName, err)
		}
		if processes.Complete && len(processes.ProcessIDs) == 0 {
			return nil
		}
		if elapsed >= windowsLegacyProcessExitWindow {
			if !processes.Complete {
				return fmt.Errorf("verify managed process exit for scheduled task %q: process paths unavailable for PIDs %v", w.taskName, processes.UnreadablePIDs)
			}
			return fmt.Errorf("managed processes for scheduled task %q did not exit within %s (processes: %v)", w.taskName, windowsLegacyProcessExitWindow, processes.ProcessIDs)
		}
		if err := windowsTaskSleep(ctx, windowsLegacyProcessPollInterval); err != nil {
			return fmt.Errorf("wait for managed process exit for scheduled task %q: %w", w.taskName, err)
		}
	}
}

func (w *windowsInstaller) matchesLegacyTask(task windowsTaskSnapshot) bool {
	if !task.Exists || task.ActionCount != 1 || task.TaskPath != `\` || !strings.EqualFold(task.TaskName, w.taskName) {
		return false
	}
	if !strings.EqualFold(normalizeWindowsTaskPath(task.Execute), normalizeWindowsTaskPath(w.binPath)) {
		return false
	}
	args := strings.Fields(strings.ToLower(strings.TrimSpace(task.Arguments)))
	return (len(args) == 1 && args[0] == "daemon") ||
		(len(args) == 2 && args[0] == "daemon" && args[1] == "--headless")
}

func normalizeWindowsTaskPath(path string) string {
	path = strings.Trim(strings.TrimSpace(path), `"`)
	return strings.ReplaceAll(path, "/", `\`)
}

func queryWindowsAgentTeamTask(parent context.Context) (windowsTaskSnapshot, error) {
	out, err := runWindowsPowerShell(parent, windowsLoopTaskSnapshotScript, "")
	if err != nil {
		return windowsTaskSnapshot{}, fmt.Errorf("query root scheduled task %q: %w: %s", "AgentTeam", err, strings.TrimSpace(string(out)))
	}
	var task windowsTaskSnapshot
	if err := decodeWindowsPowerShellJSON(out, &task); err != nil {
		return windowsTaskSnapshot{}, fmt.Errorf("decode root scheduled task %q state: %w", "AgentTeam", err)
	}
	return task, nil
}

func queryWindowsAgentTeamProcesses(parent context.Context, targetPath string) (windowsProcessSnapshot, error) {
	out, err := runWindowsPowerShell(parent, windowsAgentTeamProcessSnapshotScript, targetPath)
	if err != nil {
		return windowsProcessSnapshot{}, fmt.Errorf("query AgentTeam process paths: %w: %s", err, strings.TrimSpace(string(out)))
	}
	var processes windowsProcessSnapshot
	if err := decodeWindowsPowerShellJSON(out, &processes); err != nil {
		return windowsProcessSnapshot{}, fmt.Errorf("decode AgentTeam process paths: %w", err)
	}
	return processes, nil
}

func runWindowsPowerShell(parent context.Context, script, processPath string) ([]byte, error) {
	ctx, cancel := context.WithTimeout(parent, windowsPowerShellQueryTimeout)
	defer cancel()
	cmd := serviceCommandContext(
		ctx,
		"powershell.exe",
		"-NoProfile",
		"-NonInteractive",
		"-ExecutionPolicy", "Bypass",
		"-Command", script,
	)
	if processPath != "" {
		cmd.Env = append(os.Environ(), "AGENTTEAM_LEGACY_PROCESS_PATH="+processPath)
	}
	out, err := cmd.CombinedOutput()
	if ctx.Err() != nil {
		return out, fmt.Errorf("PowerShell query: %w", ctx.Err())
	}
	return out, err
}

func decodeWindowsPowerShellJSON(encoded []byte, target any) error {
	text := strings.TrimPrefix(strings.TrimSpace(string(encoded)), "\ufeff")
	return json.Unmarshal([]byte(text), target)
}

func sleepWindowsTaskContext(ctx context.Context, delay time.Duration) error {
	timer := time.NewTimer(delay)
	defer timer.Stop()
	select {
	case <-timer.C:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}
