package vault

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

const (
	retentionStateFilename = ".agentteam-memory-retention.json"
	retentionStateVersion  = 1
)

// RetentionState is Desktop's local metadata for a managed Memory clone.
//
// The file lives next to the clone directory rather than inside it so watcher
// commits never sync local authorization state into the managed Memory repo.
// While access is bound, RevokedAt is empty. When access is revoked, Detach
// removes .git from the clone and rewrites the same state with RevokedAt set.
type RetentionState struct {
	SchemaVersion int    `json:"schema_version"`
	MemoryID      string `json:"memory_id"`
	AgentID       string `json:"agent_id,omitempty"`
	Branch        string `json:"branch"`
	RevokedAt     string `json:"revoked_at,omitempty"`
}

// RetentionStatePath returns the local metadata path for targetPath. The
// targetPath itself is the clone directory (normally <agent-workdir>/memory).
func RetentionStatePath(targetPath string) string {
	cleanTarget := filepath.Clean(targetPath)
	parent := filepath.Dir(cleanTarget)
	return filepath.Join(parent, retentionStateFilename)
}

// ReadRetentionState reads the local Memory retention metadata for targetPath.
func ReadRetentionState(targetPath string) (RetentionState, error) {
	data, err := os.ReadFile(RetentionStatePath(targetPath))
	if err != nil {
		return RetentionState{}, err
	}
	var state RetentionState
	if err := json.Unmarshal(data, &state); err != nil {
		return RetentionState{}, fmt.Errorf("vault: decode memory retention state: %w", err)
	}
	return state, nil
}

func writeActiveRetentionState(targetPath, memoryID, agentID, branch string) error {
	return writeRetentionState(targetPath, RetentionState{
		SchemaVersion: retentionStateVersion,
		MemoryID:      strings.TrimSpace(memoryID),
		AgentID:       strings.TrimSpace(agentID),
		Branch:        strings.TrimSpace(branch),
	})
}

func writeRevokedRetentionState(targetPath, memoryID, agentID, branch string, revokedAt time.Time) error {
	if revokedAt.IsZero() {
		revokedAt = time.Now()
	}
	return writeRetentionState(targetPath, RetentionState{
		SchemaVersion: retentionStateVersion,
		MemoryID:      strings.TrimSpace(memoryID),
		AgentID:       strings.TrimSpace(agentID),
		Branch:        strings.TrimSpace(branch),
		RevokedAt:     revokedAt.UTC().Format(time.RFC3339Nano),
	})
}

func writeRetentionState(targetPath string, state RetentionState) error {
	if strings.TrimSpace(targetPath) == "" {
		return fmt.Errorf("vault: memory retention target path is required")
	}
	if strings.TrimSpace(state.MemoryID) == "" {
		return fmt.Errorf("vault: memory id is required")
	}
	if strings.TrimSpace(state.Branch) == "" {
		return fmt.Errorf("vault: branch is required")
	}
	state.SchemaVersion = retentionStateVersion
	state.MemoryID = strings.TrimSpace(state.MemoryID)
	state.AgentID = strings.TrimSpace(state.AgentID)
	state.Branch = strings.TrimSpace(state.Branch)

	path := RetentionStatePath(targetPath)
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("vault: mkdir memory retention state parent: %w", err)
	}
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("vault: encode memory retention state: %w", err)
	}
	data = append(data, '\n')
	if err := os.WriteFile(path, data, 0o644); err != nil {
		return fmt.Errorf("vault: write memory retention state: %w", err)
	}
	return nil
}

// RetainOfflineOpts configures RetainOffline.
type RetainOfflineOpts struct {
	// TargetPath is the clone directory to detach.
	TargetPath string
	// MemoryID is the managed Memory id whose clone is being retained.
	MemoryID string
	// AgentID is the agent that owned the clone. Optional but retained when
	// known so re-grant recognition can avoid cross-agent reuse.
	AgentID string
	// Branch is the explicit bound branch.
	Branch string
	// RevokedAt is the revocation timestamp. Zero means time.Now().
	RevokedAt time.Time
}

// RetainOffline detaches a clone from git sync while preserving its files and
// records local metadata that lets a future grant recognize the retained copy.
func RetainOffline(ctx context.Context, opts RetainOfflineOpts) error {
	targetPath := strings.TrimSpace(opts.TargetPath)
	memoryID := strings.TrimSpace(opts.MemoryID)
	agentID := strings.TrimSpace(opts.AgentID)
	branch := strings.TrimSpace(opts.Branch)
	if targetPath == "" {
		return fmt.Errorf("vault: memory retention target path is required")
	}
	if memoryID == "" {
		return fmt.Errorf("vault: memory id is required")
	}
	if branch == "" {
		return fmt.Errorf("vault: branch is required")
	}
	unlock, err := lockMemoryPath(ctx, targetPath)
	if err != nil {
		return fmt.Errorf("vault: acquire memory path lock: %w", err)
	}
	defer unlock()

	clone := &Clone{
		memoryID: memoryID,
		agentID:  agentID,
		branch:   branch,
		path:     targetPath,
	}
	if err := clone.Detach(ctx); err != nil {
		return err
	}
	if err := writeRevokedRetentionState(targetPath, memoryID, agentID, branch, opts.RevokedAt); err != nil {
		return err
	}
	return nil
}

// RetainOffline detaches this clone from git sync and records local retention
// metadata. It does not push; callers that own a live watcher should close and
// push before calling this method.
func (c *Clone) RetainOffline(ctx context.Context, revokedAt time.Time) error {
	if c == nil {
		return fmt.Errorf("vault: nil clone")
	}
	return RetainOffline(ctx, RetainOfflineOpts{
		TargetPath: c.path,
		MemoryID:   c.memoryID,
		AgentID:    c.agentID,
		Branch:     c.branch,
		RevokedAt:  revokedAt,
	})
}

func matchingRetainedOfflineState(targetPath, memoryID, agentID, branch string) (RetentionState, bool, error) {
	info, err := os.Stat(targetPath)
	if err != nil {
		if os.IsNotExist(err) {
			return RetentionState{}, false, nil
		}
		return RetentionState{}, false, fmt.Errorf("vault: stat retained memory path: %w", err)
	}
	if !info.IsDir() {
		return RetentionState{}, false, nil
	}
	if _, err := os.Stat(filepath.Join(targetPath, ".git")); err == nil {
		return RetentionState{}, false, nil
	} else if err != nil && !os.IsNotExist(err) {
		return RetentionState{}, false, fmt.Errorf("vault: stat retained memory .git: %w", err)
	}
	if isEmptyDir(targetPath) {
		return RetentionState{}, false, nil
	}

	state, err := ReadRetentionState(targetPath)
	if err != nil {
		if os.IsNotExist(err) {
			return RetentionState{}, false, nil
		}
		return RetentionState{}, false, err
	}
	if state.SchemaVersion != retentionStateVersion {
		return RetentionState{}, false, nil
	}
	if strings.TrimSpace(state.RevokedAt) == "" {
		return RetentionState{}, false, nil
	}
	if strings.TrimSpace(state.MemoryID) != strings.TrimSpace(memoryID) {
		return RetentionState{}, false, nil
	}
	if strings.TrimSpace(state.Branch) != strings.TrimSpace(branch) {
		return RetentionState{}, false, nil
	}
	if state.AgentID != "" && strings.TrimSpace(state.AgentID) != strings.TrimSpace(agentID) {
		return RetentionState{}, false, nil
	}
	return state, true, nil
}
