package daemoncontrol

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/x/proc"
)

const receiptFilename = "daemon.json"

// ReceiptStore persists the daemon receipt within one private runtime
// directory.
type ReceiptStore struct {
	runtimeDir string
	useProfile bool
}

// NewReceiptStore returns a receipt store rooted at runtimeDir. It is intended
// for tests and callers with an explicit private runtime directory.
func NewReceiptStore(runtimeDir string) *ReceiptStore {
	return &ReceiptStore{runtimeDir: runtimeDir}
}

// DefaultReceiptStore returns the production receipt store.
func DefaultReceiptStore() *ReceiptStore {
	return &ReceiptStore{runtimeDir: profile.RuntimeDir(), useProfile: true}
}

// VerifySecurity validates only the receipt file's type and OS access policy.
// Electron uses this boundary before parsing a recognized legacy receipt during
// an authenticated sidecar replacement.
func (s *ReceiptStore) VerifySecurity() error {
	_, err := s.inspectSecureReceipt()
	return err
}

// Read loads and strictly validates a receipt.
func (s *ReceiptStore) Read() (Receipt, error) {
	receipt, err := s.decode()
	if err != nil {
		return Receipt{}, err
	}
	if err := receipt.Validate(); err != nil {
		return Receipt{}, err
	}
	return receipt, nil
}

func (s *ReceiptStore) readForReconciliation() (Receipt, error) {
	receipt, err := s.decode()
	if err != nil {
		return Receipt{}, err
	}
	if err := receipt.validateForReconciliation(); err != nil {
		return Receipt{}, err
	}
	return receipt, nil
}

func (s *ReceiptStore) decode() (Receipt, error) {
	path, err := s.inspectSecureReceipt()
	if err != nil {
		return Receipt{}, err
	}

	file, err := os.Open(path)
	if err != nil {
		return Receipt{}, fmt.Errorf("%w: open receipt: %v", ErrReceiptAmbiguous, err)
	}
	defer file.Close()

	var receipt Receipt
	decoder := json.NewDecoder(file)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&receipt); err != nil {
		return Receipt{}, fmt.Errorf("%w: could not decode receipt", ErrReceiptInvalid)
	}
	var trailing any
	if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
		return Receipt{}, fmt.Errorf("%w: receipt contains trailing data", ErrReceiptInvalid)
	}
	return receipt, nil
}

func (s *ReceiptStore) inspectSecureReceipt() (string, error) {
	path := s.path()
	info, err := os.Lstat(path)
	if errors.Is(err, os.ErrNotExist) {
		return "", ErrReceiptMissing
	}
	if err != nil {
		return "", fmt.Errorf("%w: inspect receipt: %v", ErrReceiptAmbiguous, err)
	}
	if !info.Mode().IsRegular() {
		return "", fmt.Errorf("%w: receipt is not a regular file", ErrReceiptAmbiguous)
	}
	if err := validatePrivateRuntimeFile(path, info); err != nil {
		return "", fmt.Errorf("%w: receipt security: %v", ErrReceiptAmbiguous, err)
	}
	return path, nil
}

// Write atomically publishes a new receipt without replacing an existing one.
func (s *ReceiptStore) Write(receipt Receipt) error {
	if err := receipt.Validate(); err != nil {
		return err
	}
	if err := s.ensureRuntimeDir(); err != nil {
		return err
	}

	release, err := s.acquireMutationLock()
	if err != nil {
		return err
	}
	defer func() { _ = release() }()

	if _, err := s.Read(); err == nil {
		return ErrDaemonAlreadyRunning
	} else if !errors.Is(err, ErrReceiptMissing) {
		return err
	}
	return s.writeAtomic(receipt, false)
}

// RewriteMatching atomically replaces the receipt only when the durable
// current value still matches expected. Callers use this compare-and-swap for
// owner/control-token rotation so a failed rewrite leaves the old capability
// valid in both memory and storage.
func (s *ReceiptStore) RewriteMatching(expected, replacement Receipt) error {
	if err := expected.Validate(); err != nil {
		return err
	}
	if err := replacement.Validate(); err != nil {
		return err
	}
	if expected.InstanceID != replacement.InstanceID ||
		!sameIdentity(expected.Identity(), replacement.Identity()) ||
		expected.BaseURL != replacement.BaseURL ||
		expected.SchemaVersion != replacement.SchemaVersion ||
		expected.DesktopAPIVersion != replacement.DesktopAPIVersion {
		return fmt.Errorf("%w: replacement changes immutable daemon identity", ErrReceiptMismatch)
	}
	if err := s.ensureRuntimeDir(); err != nil {
		return err
	}

	release, err := s.acquireMutationLock()
	if err != nil {
		return err
	}
	defer func() { _ = release() }()

	current, err := s.Read()
	if err != nil {
		return err
	}
	if !sameReceipt(current, expected) {
		return ErrReceiptMismatch
	}
	return s.writeAtomic(replacement, true)
}

// writeAtomic persists receipt while the caller holds the mutation lock. For
// initial publication replace must be false; owner rotation uses an atomic
// platform replacement of the existing receipt.
func (s *ReceiptStore) writeAtomic(receipt Receipt, replace bool) error {
	path := s.path()
	temp, err := os.CreateTemp(s.runtimeDir, "."+receiptFilename+".tmp-*")
	if err != nil {
		return fmt.Errorf("create receipt temporary file: %w", err)
	}
	tempPath := temp.Name()
	cleanup := func() {
		_ = temp.Close()
		_ = os.Remove(tempPath)
	}
	published := false
	defer func() {
		if !published {
			cleanup()
		}
	}()

	if err := secureRuntimeFile(tempPath); err != nil {
		return fmt.Errorf("secure receipt temporary file: %w", err)
	}
	encoder := json.NewEncoder(temp)
	if err := encoder.Encode(receipt); err != nil {
		return fmt.Errorf("encode receipt: %w", err)
	}
	if err := temp.Sync(); err != nil {
		return fmt.Errorf("sync receipt temporary file: %w", err)
	}
	if err := temp.Close(); err != nil {
		return fmt.Errorf("close receipt temporary file: %w", err)
	}

	if err := publishRuntimeFile(tempPath, path, replace); err != nil {
		return fmt.Errorf("publish receipt: %w", err)
	}
	published = true
	if err := secureRuntimeFile(path); err != nil {
		return fmt.Errorf("secure published receipt: %w", err)
	}

	info, err := os.Stat(path)
	if err != nil {
		return fmt.Errorf("inspect published receipt: %w", err)
	}
	if !info.Mode().IsRegular() {
		return fmt.Errorf("%w: published receipt is not a private regular file", ErrReceiptAmbiguous)
	}
	if err := validatePrivateRuntimeFile(path, info); err != nil {
		return fmt.Errorf("%w: published receipt security: %v", ErrReceiptAmbiguous, err)
	}
	if err := syncDirectory(s.runtimeDir); err != nil {
		return fmt.Errorf("sync receipt directory: %w", err)
	}
	return nil
}

// DeleteMatching removes the receipt only when its instance and process
// identity still match.
func (s *ReceiptStore) DeleteMatching(instanceID string, identity ProcessIdentity) error {
	release, err := s.acquireMutationLock()
	if err != nil {
		return err
	}
	defer func() { _ = release() }()
	return s.deleteMatchingLocked(instanceID, identity, false)
}

// deleteMatchingLocked compares the current durable receipt before removing it.
// The caller must hold the receipt mutation lock.
func (s *ReceiptStore) deleteMatchingLocked(instanceID string, identity ProcessIdentity, allowLegacy bool) error {
	var current Receipt
	var err error
	if allowLegacy {
		current, err = s.readForReconciliation()
	} else {
		current, err = s.Read()
	}
	if err != nil {
		return err
	}
	if current.InstanceID != instanceID || !sameIdentity(current.Identity(), identity) {
		return ErrReceiptMismatch
	}
	if err := os.Remove(s.path()); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return ErrReceiptMismatch
		}
		return fmt.Errorf("delete daemon receipt: %w", err)
	}
	if err := syncDirectory(s.runtimeDir); err != nil {
		return fmt.Errorf("sync receipt directory: %w", err)
	}
	return nil
}

// Reconcile removes stale receipts and refuses to proceed when the recorded
// daemon process is still running.
func (s *ReceiptStore) Reconcile(lookup func(int) (proc.ProcessInfo, bool)) error {
	receipt, err := s.readForReconciliation()
	if errors.Is(err, ErrReceiptMissing) {
		return nil
	}
	if err != nil {
		return err
	}
	if lookup == nil {
		return fmt.Errorf("%w: process lookup is unavailable", ErrReceiptAmbiguous)
	}

	process, found := lookup(receipt.PID)
	if found {
		if process.PID != receipt.PID || process.StartedAt.IsZero() {
			return fmt.Errorf("%w: process identity lookup was incomplete", ErrReceiptAmbiguous)
		}
		if receipt.Identity().Matches(process) {
			return ErrDaemonAlreadyRunning
		}
	} else if proc.PIDAlive(receipt.PID) {
		return fmt.Errorf("%w: process identity lookup was inconclusive", ErrReceiptAmbiguous)
	}

	release, err := s.acquireMutationLock()
	if err != nil {
		return err
	}
	defer func() { _ = release() }()
	return s.deleteMatchingLocked(receipt.InstanceID, receipt.Identity(), true)
}

func (s *ReceiptStore) ensureRuntimeDir() error {
	if s == nil || s.runtimeDir == "" {
		return fmt.Errorf("%w: runtime directory is empty", ErrReceiptInvalid)
	}
	if s.useProfile {
		dir, err := profile.EnsureRuntimeDir()
		if err != nil {
			return err
		}
		if dir != s.runtimeDir {
			return fmt.Errorf("%w: profile runtime directory changed", ErrReceiptAmbiguous)
		}
		return nil
	}
	if err := ensurePrivateRuntimeDir(s.runtimeDir); err != nil {
		return fmt.Errorf("secure receipt runtime directory: %w", err)
	}
	info, err := os.Stat(s.runtimeDir)
	if err != nil {
		return fmt.Errorf("inspect receipt runtime directory: %w", err)
	}
	if !info.IsDir() {
		return fmt.Errorf("%w: receipt runtime path is not a directory", ErrReceiptAmbiguous)
	}
	return nil
}

func (s *ReceiptStore) path() string {
	if s == nil {
		return receiptFilename
	}
	return filepath.Join(s.runtimeDir, receiptFilename)
}

func sameIdentity(a, b ProcessIdentity) bool {
	return a.PID == b.PID &&
		a.StartedAt.Equal(b.StartedAt) &&
		a.StartID == b.StartID
}

func sameReceipt(a, b Receipt) bool {
	return a.SchemaVersion == b.SchemaVersion &&
		a.DesktopAPIVersion == b.DesktopAPIVersion &&
		a.InstanceID == b.InstanceID &&
		sameIdentity(a.Identity(), b.Identity()) &&
		a.BaseURL == b.BaseURL &&
		a.Owner.Kind == b.Owner.Kind &&
		a.Owner.ID == b.Owner.ID &&
		sameIdentity(a.Owner.ProcessIdentity, b.Owner.ProcessIdentity) &&
		a.ControlToken == b.ControlToken
}

func (s *ReceiptStore) acquireMutationLock() (func() error, error) {
	const (
		retryInterval = 5 * time.Millisecond
		waitTimeout   = 5 * time.Second
	)
	lockPath := filepath.Join(s.runtimeDir, "."+receiptFilename+".write.lock")
	lock, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("%w: open receipt mutation lock: %v", ErrReceiptAmbiguous, err)
	}
	if err := secureRuntimeFile(lockPath); err != nil {
		_ = lock.Close()
		return nil, fmt.Errorf("%w: secure receipt mutation lock: %v", ErrReceiptAmbiguous, err)
	}
	deadline := time.Now().Add(waitTimeout)
	for {
		acquired, err := tryLockMutationFile(lock)
		if err != nil {
			_ = lock.Close()
			return nil, fmt.Errorf("%w: acquire receipt mutation lock: %v", ErrReceiptAmbiguous, err)
		}
		if acquired {
			break
		}
		if time.Now().After(deadline) {
			_ = lock.Close()
			return nil, fmt.Errorf("%w: receipt mutation lock remained busy", ErrReceiptAmbiguous)
		}
		retryTimer := time.NewTimer(retryInterval)
		select {
		case <-retryTimer.C:
		}
	}
	return func() error {
		return errors.Join(unlockMutationFile(lock), lock.Close())
	}, nil
}
