// Package runtimecredential owns atomically replaceable, process-lifetime
// credential files beneath AgentTeam's private runtime directory.
package runtimecredential

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"

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

const credentialFilename = "token"
const credentialDirPrefix = "exec-credential-"

// Credential is one private file whose stable path can be inherited by a
// long-running process while its contents are atomically replaced.
type Credential struct {
	mu      sync.Mutex
	dir     string
	path    string
	closed  bool
	cleaned bool
}

// Create allocates a random private directory and publishes the initial value.
func Create(value string) (*Credential, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil, errors.New("runtime credential value is empty")
	}
	root, err := profile.EnsureRuntimeDir()
	if err != nil {
		return nil, err
	}
	dir, err := os.MkdirTemp(root, credentialDirPrefix)
	if err != nil {
		return nil, fmt.Errorf("create runtime credential directory: %w", err)
	}
	cleanup := true
	defer func() {
		if cleanup {
			_ = os.RemoveAll(dir)
		}
	}()
	if err := profile.SecureRuntimeDir(dir); err != nil {
		return nil, fmt.Errorf("secure runtime credential directory: %w", err)
	}
	credential := &Credential{dir: dir, path: filepath.Join(dir, credentialFilename)}
	if err := credential.replaceLocked(value); err != nil {
		return nil, err
	}
	cleanup = false
	return credential, nil
}

// CleanupStale removes credential directories left by a previous crashed
// daemon. The daemon calls this only after acquiring its process-wide instance
// lock, so no live instance can own a matching directory concurrently.
func CleanupStale() error {
	root, err := profile.EnsureRuntimeDir()
	if err != nil {
		return errors.New("prepare runtime credential cleanup: unavailable")
	}
	entries, err := os.ReadDir(root)
	if err != nil {
		return errors.New("list stale runtime credentials: unavailable")
	}
	failed := 0
	for _, entry := range entries {
		if !strings.HasPrefix(entry.Name(), credentialDirPrefix) {
			continue
		}
		if err := os.RemoveAll(filepath.Join(root, entry.Name())); err != nil {
			failed++
		}
	}
	if failed > 0 {
		return fmt.Errorf("remove stale runtime credentials: %d cleanup failures", failed)
	}
	return nil
}

// Path returns the stable absolute file path inherited by the runtime.
func (c *Credential) Path() string {
	if c == nil {
		return ""
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return ""
	}
	return c.path
}

// Replace atomically publishes a complete replacement value. A failed replace
// leaves the previous file visible and unchanged.
func (c *Credential) Replace(value string) error {
	if c == nil {
		return errors.New("runtime credential is unavailable")
	}
	value = strings.TrimSpace(value)
	if value == "" {
		return errors.New("runtime credential value is empty")
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.closed {
		return errors.New("runtime credential is closed")
	}
	return c.replaceLocked(value)
}

// Close removes the credential and its private directory. It serializes with
// Replace so a late rotation cannot recreate the file after cleanup.
func (c *Credential) Close() error {
	if c == nil {
		return nil
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.cleaned {
		return nil
	}
	c.closed = true

	// The random private directory is wholly owned by this Credential. Remove
	// its complete tree so a failed publication, scanner, or runtime-created
	// unknown entry cannot leave permanent ENOTEMPTY cleanup debt. RemoveAll
	// removes symlink directory entries themselves rather than following their
	// targets. The platform helper retries transient Windows sharing failures;
	// Close itself also remains retryable if a partial tree is still busy.
	if err := removeCredentialTree(c.dir); err != nil {
		return fmt.Errorf("remove runtime credential directory: %w", err)
	}
	c.cleaned = true
	return nil
}

func (c *Credential) replaceLocked(value string) error {
	temp, err := os.CreateTemp(c.dir, ".token.tmp-*")
	if err != nil {
		return fmt.Errorf("create runtime credential temporary file: %w", err)
	}
	tempPath := temp.Name()
	committed := false
	defer func() {
		_ = temp.Close()
		if !committed {
			_ = os.Remove(tempPath)
		}
	}()

	if err := profile.SecureRuntimeFile(tempPath); err != nil {
		return fmt.Errorf("secure runtime credential temporary file: %w", err)
	}
	if _, err := temp.WriteString(value + "\n"); err != nil {
		return fmt.Errorf("write runtime credential temporary file: %w", err)
	}
	if err := temp.Sync(); err != nil {
		return fmt.Errorf("sync runtime credential temporary file: %w", err)
	}
	if err := temp.Close(); err != nil {
		return fmt.Errorf("close runtime credential temporary file: %w", err)
	}
	info, err := os.Lstat(tempPath)
	if err != nil {
		return fmt.Errorf("inspect runtime credential temporary file: %w", err)
	}
	if err := validatePrivateCredentialFile(tempPath, info); err != nil {
		return fmt.Errorf("validate runtime credential temporary file: %w", err)
	}
	if err := replaceCredentialFile(tempPath, c.path); err != nil {
		return fmt.Errorf("publish runtime credential file: %w", err)
	}
	// The rename/MoveFileEx above is the commit point. The temporary file was
	// already synced and secured, so never report a rollback-style error after
	// the target has changed. Directory sync is best-effort because daemon crash
	// also discards the in-memory active-token index.
	committed = true
	_ = syncCredentialDirectory(c.dir)
	return nil
}
