package desktop

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/tidbcloud/AgentTeam/x/proc"
)

const (
	daemonReceiptFilename        = "daemon.json"
	developmentDesktopAPIVersion = 2
)

type developmentDaemonReceipt struct {
	SchemaVersion     int                    `json:"schemaVersion"`
	DesktopAPIVersion int                    `json:"desktopApiVersion"`
	InstanceID        string                 `json:"instanceId"`
	PID               int                    `json:"pid"`
	StartedAt         time.Time              `json:"processStartedAt"`
	StartID           string                 `json:"processStartId,omitempty"`
	BaseURL           string                 `json:"baseURL"`
	Owner             developmentDaemonOwner `json:"owner"`
	ControlToken      string                 `json:"controlToken"`
}

type developmentDaemonOwner struct {
	Kind      string    `json:"kind"`
	ID        string    `json:"id"`
	PID       int       `json:"pid"`
	StartedAt time.Time `json:"processStartedAt"`
	StartID   string    `json:"processStartId,omitempty"`
}

type ownedDaemonCleaner struct {
	lookupProcess    func(int) (proc.ProcessInfo, bool)
	pidAlive         func(int) bool
	terminateProcess func(int, time.Duration)
}

func newOwnedDaemonCleaner() ownedDaemonCleaner {
	return ownedDaemonCleaner{
		lookupProcess: proc.ProcessByPID,
		pidAlive:      proc.PIDAlive,
		terminateProcess: func(pid int, timeout time.Duration) {
			proc.TerminateProcess(pid, nil, timeout)
		},
	}
}

func (cleaner ownedDaemonCleaner) cleanup(ctx context.Context, configDir string) error {
	if cleaner.lookupProcess == nil {
		cleaner.lookupProcess = proc.ProcessByPID
	}
	if cleaner.pidAlive == nil {
		cleaner.pidAlive = func(int) bool { return false }
	}
	if cleaner.terminateProcess == nil {
		cleaner.terminateProcess = func(pid int, timeout time.Duration) {
			proc.TerminateProcess(pid, nil, timeout)
		}
	}

	receiptPath := filepath.Join(configDir, "run", daemonReceiptFilename)
	raw, receipt, err := readDevelopmentDaemonReceipt(receiptPath)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}

	if processIdentityMatches(cleaner.lookupProcess, proc.ProcessInfo{
		PID:       receipt.Owner.PID,
		StartedAt: receipt.Owner.StartedAt,
		StartID:   receipt.Owner.StartID,
	}) {
		return fmt.Errorf(
			"development daemon %d still has active Electron owner %d",
			receipt.PID,
			receipt.Owner.PID,
		)
	}
	if _, found := cleaner.lookupProcess(receipt.Owner.PID); !found && cleaner.pidAlive(receipt.Owner.PID) {
		return fmt.Errorf(
			"development daemon owner process %d is alive but its identity is ambiguous",
			receipt.Owner.PID,
		)
	}

	if processIdentityMatches(cleaner.lookupProcess, proc.ProcessInfo{
		PID:       receipt.PID,
		StartedAt: receipt.StartedAt,
		StartID:   receipt.StartID,
	}) {
		if err := ctx.Err(); err != nil {
			return err
		}
		cleaner.terminateProcess(receipt.PID, 2*time.Second)
		if err := waitForProcessLifetimeToEnd(ctx, cleaner.lookupProcess, proc.ProcessInfo{
			PID:       receipt.PID,
			StartedAt: receipt.StartedAt,
			StartID:   receipt.StartID,
		}); err != nil {
			return err
		}
	} else if _, found := cleaner.lookupProcess(receipt.PID); !found && cleaner.pidAlive(receipt.PID) {
		return fmt.Errorf(
			"development daemon process %d is alive but its identity is ambiguous",
			receipt.PID,
		)
	}

	current, err := os.ReadFile(receiptPath)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("re-read development daemon receipt: %w", err)
	}
	if !bytes.Equal(current, raw) {
		return fmt.Errorf("development daemon receipt changed during cleanup")
	}
	if err := os.Remove(receiptPath); err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("remove stale development daemon receipt: %w", err)
	}
	return nil
}

func readDevelopmentDaemonReceipt(path string) ([]byte, developmentDaemonReceipt, error) {
	info, err := os.Lstat(path)
	if err != nil {
		return nil, developmentDaemonReceipt{}, err
	}
	if !info.Mode().IsRegular() {
		return nil, developmentDaemonReceipt{}, fmt.Errorf(
			"development daemon receipt %s is not a regular file",
			path,
		)
	}
	raw, err := os.ReadFile(path)
	if err != nil {
		return nil, developmentDaemonReceipt{}, fmt.Errorf(
			"read development daemon receipt: %w",
			err,
		)
	}
	var receipt developmentDaemonReceipt
	if err := json.Unmarshal(raw, &receipt); err != nil {
		return nil, developmentDaemonReceipt{}, fmt.Errorf(
			"decode development daemon receipt: %w",
			err,
		)
	}
	if receipt.SchemaVersion != 1 ||
		receipt.DesktopAPIVersion != developmentDesktopAPIVersion ||
		receipt.InstanceID == "" ||
		receipt.PID <= 0 ||
		receipt.StartedAt.IsZero() ||
		receipt.BaseURL == "" ||
		receipt.Owner.Kind != "electron" ||
		receipt.Owner.ID == "" ||
		receipt.Owner.PID <= 0 ||
		receipt.Owner.StartedAt.IsZero() ||
		receipt.ControlToken == "" {
		return nil, developmentDaemonReceipt{}, fmt.Errorf(
			"development daemon receipt is invalid or is not Electron-owned",
		)
	}
	return raw, receipt, nil
}

func processIdentityMatches(
	lookup func(int) (proc.ProcessInfo, bool),
	identity proc.ProcessInfo,
) bool {
	process, found := lookup(identity.PID)
	return found && proc.ProcessIdentityMatches(identity, process)
}

func waitForProcessLifetimeToEnd(
	ctx context.Context,
	lookup func(int) (proc.ProcessInfo, bool),
	identity proc.ProcessInfo,
) error {
	timer := time.NewTicker(50 * time.Millisecond)
	defer timer.Stop()
	timeout := time.NewTimer(5 * time.Second)
	defer timeout.Stop()
	for {
		if !processIdentityMatches(lookup, identity) {
			return nil
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-timeout.C:
			return fmt.Errorf("development daemon process %d did not exit", identity.PID)
		case <-timer.C:
		}
	}
}
