// Package daemon manages the agentteam background process lifecycle,
// including authenticated runtime discovery, process status tracking,
// service integration, log rotation, and channel/dispatcher wiring.
package daemon

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"log"
	"log/slog"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"gopkg.in/natefinch/lumberjack.v2"

	"github.com/tidbcloud/AgentTeam/desktop/internal/api"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/exec"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/localsecurity"
	"github.com/tidbcloud/AgentTeam/desktop/internal/api/mcp"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/daemoncontrol"
	"github.com/tidbcloud/AgentTeam/desktop/internal/dispatcher"
	"github.com/tidbcloud/AgentTeam/desktop/internal/execruntimeenv"
	"github.com/tidbcloud/AgentTeam/desktop/internal/i18n"
	"github.com/tidbcloud/AgentTeam/desktop/internal/metrics"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/runtimecredential"
	"github.com/tidbcloud/AgentTeam/desktop/internal/streamregistry"
	"github.com/tidbcloud/AgentTeam/desktop/internal/token"
	"github.com/tidbcloud/AgentTeam/desktop/internal/tokenanalysis"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
	"github.com/tidbcloud/AgentTeam/desktop/internal/uisession"
	"github.com/tidbcloud/AgentTeam/desktop/internal/version"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire"
	"github.com/tidbcloud/AgentTeam/desktop/internal/wire/manager"
	"github.com/tidbcloud/AgentTeam/x/proc"
)

type Daemon struct {
	cfg                            *profile.Config
	shutdownOnce                   sync.Once
	cancel                         context.CancelFunc
	logWriter                      io.WriteCloser
	previousLogOutput              io.Writer
	previousLogFlags               int
	previousSlog                   *slog.Logger
	runtimeDetectStarted           bool
	runtimeDetectChangeUnsubscribe func()
	profileWatcherStarted          bool
	metricsStarted                 bool
	apiServer                      *http.Server
	shutdownAPIServer              func(context.Context) error
	apiShutdownTimeout             time.Duration
	dispatcher                     *dispatcher.Dispatcher
	serverConnections              *manager.Manager
	uiSessions                     *uisession.Store
	streamRegistry                 *streamregistry.Registry
	messageService                 *mcp.MessageService
	configPath                     string
	startupError                   error
	listener                       net.Listener
	baseURL                        string
	serverErr                      chan error
	shutdownRequested              chan struct{}
	shutdownRequestOnce            sync.Once
	instanceLock                   *InstanceLock
	receiptStore                   *daemoncontrol.ReceiptStore
	receipt                        *daemoncontrol.Receipt
	receiptPublished               bool
	controller                     *daemoncontrol.Controller
	tokenAnalysis                  *tokenanalysis.Service
}

// OwnerBootstrap identifies the process that owns this daemon lifetime. CLI
// and service owners may omit ID and PID; the daemon generates an ID and uses
// its own process identity. Electron owners must provide both.
type OwnerBootstrap struct {
	Kind daemoncontrol.OwnerKind
	ID   string
	PID  int
}

// RunOptions is the only daemon startup API. It deliberately contains no UI
// mode and no secret material.
type RunOptions struct {
	OwnerBootstrap OwnerBootstrap
	BindIntent     BindIntent
}

type resolvedRunOptions struct {
	owner daemoncontrol.Owner
	bind  BindIntent
}

// New creates a Daemon, loading config from configPath.
// If the config cannot be loaded, a Daemon with no config is returned
// (Run will fail with a clear error in that case).
func New(configPath string) *Daemon {
	if configPath == "" {
		configPath = profile.Path()
	}
	d := &Daemon{configPath: configPath}
	// New performs shell/profile probes before Run. Clear a parent runtime's
	// capability first so even those startup helpers cannot inherit it. Run
	// repeats this fail-closed boundary for directly constructed Daemon values.
	if err := execruntimeenv.ClearProcess(); err != nil {
		d.startupError = errors.New("clear inherited Exec runtime environment: unavailable")
		return d
	}
	// Load environment variables from the user's shell before loading the config
	// so runtime env-key lookups succeed. Profile scanning remains a fallback for
	// simple exported values if the shell probe cannot provide them. PATH is
	// cached separately for runtime detection and launch only.
	profile.LoadEnvFromShell()
	profile.LoadEnvFromShellProfiles()
	profile.LoadPathFromShell()

	cfg, err := profile.Load(configPath)
	if err == nil {
		d.cfg = cfg
	}
	return d
}

// Run starts the daemon's single non-GUI execution path and blocks until
// shutdown.
func (d *Daemon) Run(options RunOptions) error {
	// A daemon started from inside another AgentTeam runtime must not propagate that
	// parent's Exec capability to probes, helpers, or newly managed runtimes.
	// Each runtime receives a distinct daemon-authored authorization later.
	if err := execruntimeenv.ClearProcess(); err != nil {
		return errors.New("clear inherited Exec runtime environment: unavailable")
	}
	if d.startupError != nil {
		return d.startupError
	}
	if d.cfg == nil {
		return fmt.Errorf("failed to load config")
	}
	resolved, err := resolveRunOptions(options)
	if err != nil {
		return err
	}
	if err := profile.EnsureDir(); err != nil {
		return fmt.Errorf("failed to create config directory: %w", err)
	}

	// Process-lifetime startup ordering is strict: lock, stale receipt
	// reconciliation, listener reservation, services/router, server start,
	// receipt publication, then readiness reporting.
	instanceLock, err := AcquireInstanceLock()
	if err != nil {
		return err
	}
	d.instanceLock = instanceLock
	if err := reconcileLegacyWindowsService(resolved.owner.Kind); err != nil {
		d.shutdown()
		return fmt.Errorf("reconcile legacy Windows daemon service: %w", err)
	}
	if err := runtimecredential.CleanupStale(); err != nil {
		slog.Warn("stale Exec credential cleanup failed", "reason", "unavailable")
	}
	d.receiptStore = daemoncontrol.DefaultReceiptStore()
	if err := d.receiptStore.Reconcile(proc.ProcessByPID); err != nil {
		d.shutdown()
		return fmt.Errorf("reconcile daemon receipt: %w", err)
	}
	listener, baseURL, err := buildListener(resolved.owner.Kind, resolved.bind)
	if err != nil {
		d.shutdown()
		return err
	}
	d.listener = listener
	d.baseURL = baseURL

	receipt, err := newDaemonReceipt(resolved.owner, baseURL)
	if err != nil {
		d.shutdown()
		return err
	}
	// Construct the instance identity before the router so the control plane
	// built in the next startup phase can bind to this exact instance. Keep it
	// unpublished until the HTTP server has been constructed and started.
	d.receipt = &receipt
	d.controller, err = daemoncontrol.NewController(daemoncontrol.ControllerOptions{
		Receipt: receipt,
		Store:   d.receiptStore,
	})
	if err != nil {
		d.shutdown()
		return fmt.Errorf("construct daemon controller: %w", err)
	}
	d.shutdownRequested = make(chan struct{})
	if err := d.setupServices(listener, baseURL); err != nil {
		d.shutdown()
		return fmt.Errorf("failed to start services: %w", err)
	}
	if err := d.receiptStore.Write(receipt); err != nil {
		d.shutdown()
		return fmt.Errorf("publish daemon receipt: %w", err)
	}
	d.receiptPublished = true

	slog.Info("agentteam is ready",
		"base_url", baseURL,
		"pid", receipt.PID,
		"instance_id", receipt.InstanceID,
		"owner_kind", receipt.Owner.Kind,
	)
	return d.wait()
}

func resolveRunOptions(options RunOptions) (resolvedRunOptions, error) {
	bootstrap := options.OwnerBootstrap
	switch bootstrap.Kind {
	case daemoncontrol.OwnerKindCLI, daemoncontrol.OwnerKindService:
		if bootstrap.PID == 0 {
			bootstrap.PID = os.Getpid()
		}
		if strings.TrimSpace(bootstrap.ID) == "" {
			id, err := randomCredential("own_", 18)
			if err != nil {
				return resolvedRunOptions{}, fmt.Errorf("generate owner id: %w", err)
			}
			bootstrap.ID = id
		}
	case daemoncontrol.OwnerKindElectron:
		if strings.TrimSpace(bootstrap.ID) == "" {
			return resolvedRunOptions{}, fmt.Errorf("electron owner id is required")
		}
		if bootstrap.PID <= 0 {
			return resolvedRunOptions{}, fmt.Errorf("electron owner pid is required")
		}
	default:
		return resolvedRunOptions{}, fmt.Errorf("unsupported daemon owner kind %q", bootstrap.Kind)
	}
	bind, err := normalizeBindIntent(bootstrap.Kind, options.BindIntent)
	if err != nil {
		return resolvedRunOptions{}, err
	}
	process, found := proc.ProcessByPID(bootstrap.PID)
	if !found || process.StartedAt.IsZero() {
		return resolvedRunOptions{}, fmt.Errorf("owner process %d identity is unavailable", bootstrap.PID)
	}
	return resolvedRunOptions{
		owner: daemoncontrol.Owner{
			Kind: bootstrap.Kind,
			ID:   strings.TrimSpace(bootstrap.ID),
			ProcessIdentity: daemoncontrol.ProcessIdentity{
				PID:       bootstrap.PID,
				StartedAt: process.StartedAt,
				StartID:   process.StartID,
			},
		},
		bind: bind,
	}, nil
}

func newDaemonReceipt(owner daemoncontrol.Owner, baseURL string) (daemoncontrol.Receipt, error) {
	process, found := proc.ProcessByPID(os.Getpid())
	if !found || process.StartedAt.IsZero() {
		return daemoncontrol.Receipt{}, fmt.Errorf("daemon process identity is unavailable")
	}
	instanceID, err := randomCredential("ins_", 18)
	if err != nil {
		return daemoncontrol.Receipt{}, fmt.Errorf("generate daemon instance id: %w", err)
	}
	controlToken, err := randomCredential("", 32)
	if err != nil {
		return daemoncontrol.Receipt{}, fmt.Errorf("generate daemon control token: %w", err)
	}
	receipt := daemoncontrol.Receipt{
		SchemaVersion:     daemoncontrol.SchemaVersion,
		DesktopAPIVersion: daemoncontrol.DesktopAPIVersion,
		InstanceID:        instanceID,
		PID:               process.PID,
		StartedAt:         process.StartedAt,
		StartID:           process.StartID,
		BaseURL:           baseURL,
		Owner:             owner,
		ControlToken:      controlToken,
	}
	if err := receipt.Validate(); err != nil {
		return daemoncontrol.Receipt{}, err
	}
	return receipt, nil
}

func randomCredential(prefix string, size int) (string, error) {
	data := make([]byte, size)
	if _, err := rand.Read(data); err != nil {
		return "", err
	}
	return prefix + base64.RawURLEncoding.EncodeToString(data), nil
}

// setupServices builds all background services and starts the HTTP server on
// the already-reserved listener.
func (d *Daemon) setupServices(listener net.Listener, baseURL string) error {
	if d.cfg == nil {
		return fmt.Errorf("config is not loaded")
	}
	if d.logWriter != nil {
		return fmt.Errorf("daemon services are already initialized")
	}

	logPath := strings.TrimSpace(d.cfg.LogFile())
	if logPath == "" {
		return fmt.Errorf("daemon log path is empty")
	}
	if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
		return fmt.Errorf("create log directory: %w", err)
	}

	lj := &lumberjack.Logger{
		Filename:   logPath,
		MaxSize:    10, // megabytes
		MaxBackups: 10,
		Compress:   false,
	}

	d.previousLogOutput = log.Writer()
	d.previousLogFlags = log.Flags()
	d.previousSlog = slog.Default()

	writer := logOutput(os.Stderr, logPath, lj)
	log.SetOutput(writer)
	log.SetFlags(log.LstdFlags | log.Lmicroseconds)
	slogLevel := profile.ParseLogLevel(d.cfg.Log.Level)
	slog.SetDefault(slog.New(slog.NewTextHandler(writer, &slog.HandlerOptions{Level: slogLevel})))
	d.logWriter = lj

	slog.Info("logging initialized", "path", logPath)

	// Initialize i18n.
	i18n.SetLang(profile.EffectiveLanguage(d.cfg))

	ctx, cancel := context.WithCancel(context.Background())
	d.cancel = cancel

	// Prune orphaned agents whose source channel was deleted since last run.
	// This removes both the agent config entries and their workspace dirs.
	profile.Default().Agents().CleanupOrphans()

	metrics.Init(ctx)
	d.metricsStarted = true
	agent.SetTaskPayloadFingerprinter(token.Default().FingerprintTokenAnalysisPayload)
	agent.SetRuntimeAnalysisTokenRevoker(func(value string) {
		token.Default().RevokeAgentToken(value)
	})
	var runtimeDispatcher *dispatcher.Dispatcher
	d.tokenAnalysis = tokenanalysis.New(tokenanalysis.Dependencies{
		Evidence:         metrics.DefaultTokenAnalysisQuerier(),
		Usage:            metrics.DefaultServerMessageCollector(),
		BuildFingerprint: version.KnownCommit(),
		Agents: func() []tokenanalysis.Agent {
			configured := profile.Default().Agents().All()
			agents := make([]tokenanalysis.Agent, 0, len(configured))
			for _, item := range configured {
				agents = append(agents, tokenanalysis.Agent{
					ID:      item.ID,
					Name:    item.Name,
					Runtime: item.Runtime,
					Model:   item.Model,
				})
			}
			return agents
		},
		CollectionChanged: func(enabled bool) {
			if enabled && runtimeDispatcher != nil {
				runtimeDispatcher.RecordActiveTokenAnalysisContext()
			}
		},
		ActiveLaunches: func() []string {
			if runtimeDispatcher == nil {
				return nil
			}
			return runtimeDispatcher.ActiveTokenAnalysisLaunches()
		},
	})

	serverManager := profile.Default().Servers()
	if err := serverManager.PrepareRuntimeMode(); err != nil {
		return fmt.Errorf("prepare server runtime mutations: %w", err)
	}

	// The daemon composition root owns one process-wide connection manager and
	// explicitly injects it into the Dispatcher and HTTP Router.
	var err error
	d.serverConnections = manager.New(nil, func(
		server profile.AgentTeamServer,
		_ types.ServerConnectionContext,
	) (types.ServerConnection, error) {
		return wire.New(server, runtimeDispatcher)
	})
	runtimeDispatcher, err = dispatcher.NewWithConnectionManager(
		baseURL, d.serverConnections,
		dispatcher.Dependencies{
			TokenAnalysisToken: token.Default().GenerateTokenAnalysisAgentToken,
		},
	)
	if err != nil {
		return err
	}
	d.dispatcher = runtimeDispatcher
	d.uiSessions = uisession.NewStore(uisession.Options{})
	d.streamRegistry = streamregistry.New()
	d.messageService = mcp.NewMessageService()
	if err := serverManager.ActivateRuntimeMode(
		api.NewServerRuntimeParticipant(d.serverConnections, d.uiSessions),
	); err != nil {
		serverManager.DeactivateRuntimeMode()
		return fmt.Errorf("activate server runtime mutations: %w", err)
	}
	if err := d.dispatcher.Start(ctx); err != nil {
		serverManager.DeactivateRuntimeMode()
		return fmt.Errorf("failed to start server connections: %w", err)
	}

	// Start the profile watcher only after server mutation runtime mode is fully
	// wired and the initial connection set is running. External server edits
	// must never observe the preparing window or silently fall back offline.
	if err := profile.Default().StartWatcher(); err != nil {
		slog.Warn("failed to start profile file watcher", "error", err)
	} else {
		d.profileWatcherStarted = true
	}

	// Start runtime detection after watcher initialization so the startup pass
	// can update watched runtime config paths without nil races.
	profile.Default().Runtimes().StartDetect()
	d.runtimeDetectStarted = true
	d.wireMemoryProvider()
	d.wireSkillProvider()
	d.wireKnowledgeBaseProvider()
	d.startManagedAssetRefreshLoop(ctx)
	d.runtimeDetectChangeUnsubscribe = profile.Default().Runtimes().OnDetectChange(d.resendMachineCapabilitiesOnRuntimeChange)

	// Create and start HTTP server.
	securityPolicy, err := localsecurity.New(localsecurity.Options{
		Host:       listener.Addr().String(),
		DevOrigins: localDevOrigins(),
	})
	if err != nil {
		return fmt.Errorf("construct local request security policy: %w", err)
	}
	router, err := api.NewRouterWithOptions(
		d.serverConnections,
		d.dispatcher,
		d.dispatcher,
		d.uiSessions,
		d.streamRegistry,
		api.RouterOptions{
			Exec: exec.RouteDependencies{
				Authorizations: d.dispatcher.ExecAuthorizer(),
				TurnTargets:    d.dispatcher,
			},
			MessageService: d.messageService,
			DaemonControl: api.DaemonControlOptions{
				Controller:      d.controller,
				RequestShutdown: d.requestShutdown,
				UISessions: api.NewDaemonUISessionControl(
					d.receipt.InstanceID,
					d.uiSessions,
				),
				LocalSecurity: securityPolicy,
			},
			TokenAnalysis: d.tokenAnalysis,
		},
	)
	if err != nil {
		return err
	}
	srv := &http.Server{Addr: listener.Addr().String(), Handler: router}
	d.apiServer = srv
	d.shutdownAPIServer = srv.Shutdown
	d.serverErr = make(chan error, 1)

	go func() {
		if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
			slog.Error("api service error", "error", err)
			d.serverErr <- err
		}
	}()
	return nil
}

func localDevOrigins() []string {
	raw := strings.TrimSpace(os.Getenv(profile.DevelEnvKey))
	if raw == "" {
		return nil
	}
	if !strings.Contains(raw, "://") {
		raw = "http://" + raw
	}
	return []string{raw}
}

// shutdown stops background components and removes only this instance's
// receipt after the existing graceful drain.
// Safe to call multiple times — only the first invocation takes effect.
func (d *Daemon) shutdown() {
	d.shutdownOnce.Do(func() {
		serverManager := profile.Default().Servers()
		if serverManager.LifecycleMode() == profile.ServerLifecycleActive {
			if err := serverManager.SuspendRuntimeMode(); err != nil {
				slog.Warn("suspend server runtime mutations", "error", err)
			}
		}
		apiDrained := true
		shutdownTimeout := d.apiShutdownTimeout
		if shutdownTimeout <= 0 {
			shutdownTimeout = 10 * time.Second
		}
		if d.streamRegistry != nil {
			waitForGatewayOps := d.streamRegistry.CancelAll()
			registryCtx, registryCancel := context.WithTimeout(
				context.Background(),
				shutdownTimeout,
			)
			if err := waitForGatewayOps(registryCtx); err != nil {
				apiDrained = false
				slog.Warn(
					"drain gateway operations before api server shutdown",
					"error", err,
				)
			}
			registryCancel()
		}
		if d.shutdownAPIServer != nil || d.apiServer != nil {
			// HTTP shutdown always gets a fresh independent budget, even when
			// registry draining timed out. Only both successful drains permit
			// the runtime lifecycle to transition Offline.
			shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout)
			var err error
			if d.shutdownAPIServer != nil {
				err = d.shutdownAPIServer(shutdownCtx)
			} else {
				err = d.apiServer.Shutdown(shutdownCtx)
			}
			shutdownCancel()
			if err != nil {
				apiDrained = false
				slog.Warn(
					"drain api server before offline profile mutations",
					"error", err,
				)
			}
		}
		// Stop channels first so no new sessions or reconnects can race
		// with session teardown below.
		if d.dispatcher != nil {
			stopCtx, stopCancel := context.WithTimeout(context.Background(), 10*time.Second)
			d.dispatcher.StopForShutdown(stopCtx)
			stopCancel()
		}

		if d.cancel != nil {
			d.cancel()
		}

		if d.runtimeDetectChangeUnsubscribe != nil {
			d.runtimeDetectChangeUnsubscribe()
			d.runtimeDetectChangeUnsubscribe = nil
		}
		if d.runtimeDetectStarted {
			profile.Default().Runtimes().StopDetect()
		}
		if d.profileWatcherStarted {
			profile.Default().StopWatcher()
		}
		if apiDrained {
			serverManager.DeactivateRuntimeMode()
		}
		if d.metricsStarted {
			if cs := metrics.DefaultServerMessageCollector(); cs != nil {
				cs.Stop()
			}
			if ls := metrics.DefaultLifecycle(); ls != nil {
				ls.Stop()
			}
			metrics.CloseTSDB()
		}
		agent.SetTaskPayloadFingerprinter(nil)
		agent.SetRuntimeAnalysisTokenRevoker(nil)
		if d.listener != nil {
			_ = d.listener.Close()
		}
		if d.receiptPublished && d.receipt != nil && d.receiptStore != nil {
			err := d.receiptStore.DeleteMatching(d.receipt.InstanceID, d.receipt.Identity())
			if err != nil && !errors.Is(err, daemoncontrol.ErrReceiptMissing) &&
				!errors.Is(err, daemoncontrol.ErrReceiptMismatch) {
				slog.Warn("remove daemon receipt", "error", err)
			}
			d.receiptPublished = false
		}
		if d.logWriter != nil {
			log.SetOutput(d.previousLogOutput)
			log.SetFlags(d.previousLogFlags)
			slog.SetDefault(d.previousSlog)
			_ = d.logWriter.Close()
			d.logWriter = nil
		}
		if d.instanceLock != nil {
			if err := d.instanceLock.Release(); err != nil {
				slog.Warn("release daemon instance lock", "error", err)
			}
			d.instanceLock = nil
		}
	})
}

// wait blocks until a signal, an unexpected HTTP server exit, or an
// authenticated owner shutdown request. The daemon deliberately outlives an
// Electron owner that crashes or is killed so its agent runtimes keep running
// and a later Electron process can adopt it through the control plane.
func (d *Daemon) wait() error {
	return proc.RunWithSignals(context.Background(), func(ctx context.Context) error {
		select {
		case <-ctx.Done():
			slog.Info("shutting down")
			return nil
		case err := <-d.serverErr:
			return fmt.Errorf("api server stopped: %w", err)
		case <-d.shutdownRequested:
			slog.Info("owner requested daemon shutdown")
			return nil
		}
	}, d.shutdown)
}

func (d *Daemon) requestShutdown() {
	if d == nil || d.shutdownRequested == nil {
		return
	}
	d.shutdownRequestOnce.Do(func() {
		close(d.shutdownRequested)
	})
}
