package kb

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const (
	sourceMirrorWorkspacePrefix          = "mirror-"
	sourceMirrorWorkspaceLockName        = ".meshx-source-mirror.lock"
	sourceMirrorWorkspaceDeletingPrefix  = "mirror-deleting-"
	sourceMirrorLifecycleLockSuffix      = ".meshx-source-mirror-lifecycle.lock"
	SourceMirrorWorkspaceCleanupInterval = 30 * time.Minute
	legacySourceMirrorWorkspaceStaleAge  = 24 * time.Hour
)

type SourceMirrorWorkspaceCleanupResult struct {
	Removed       int
	SkippedActive int
	SkippedRecent int
	Failed        int
}

type sourceMirrorWorkspace struct {
	path   string
	lock   *os.File
	logger *slog.Logger
}

func newSourceMirrorWorkspace(root string, logger *slog.Logger) (*sourceMirrorWorkspace, error) {
	lifecycleLock, err := lockSourceMirrorLifecycle(root)
	if err != nil {
		return nil, fmt.Errorf("lock source mirror workspace lifecycle: %w", err)
	}
	path, err := os.MkdirTemp(root, sourceMirrorWorkspacePrefix+"*")
	if err != nil {
		return nil, errors.Join(err, releaseSourceMirrorWorkspaceLock(lifecycleLock))
	}
	lock, acquired, err := tryLockSourceMirrorFile(filepath.Join(path, sourceMirrorWorkspaceLockName))
	if err != nil {
		_ = os.RemoveAll(path)
		return nil, errors.Join(fmt.Errorf("lock source mirror workspace: %w", err), releaseSourceMirrorWorkspaceLock(lifecycleLock))
	}
	if !acquired {
		_ = os.RemoveAll(path)
		return nil, errors.Join(fmt.Errorf("lock source mirror workspace: lock is already held"), releaseSourceMirrorWorkspaceLock(lifecycleLock))
	}
	if err := releaseSourceMirrorWorkspaceLock(lifecycleLock); err != nil {
		_ = releaseSourceMirrorWorkspaceLock(lock)
		_ = os.RemoveAll(path)
		return nil, fmt.Errorf("unlock source mirror workspace lifecycle: %w", err)
	}
	return &sourceMirrorWorkspace{path: path, lock: lock, logger: logger}, nil
}

func (w *sourceMirrorWorkspace) Close() {
	if w == nil {
		return
	}
	lifecycleLock, err := lockSourceMirrorLifecycle(filepath.Dir(w.path))
	if err != nil {
		_ = releaseSourceMirrorWorkspaceLock(w.lock)
		w.logger.Warn("source mirror workspace cleanup failed", "error_class", "lifecycle_lock_failed")
		return
	}
	_, _, err = removeSourceMirrorWorkspace(w.path, w.lock, lifecycleLock)
	if err != nil {
		w.logger.Warn("source mirror workspace cleanup failed", "error_class", "workspace_remove_failed")
	}
}

func (s AGSHTTPSourceMirrorSyncer) CleanupStaleWorkspaces(ctx context.Context, now time.Time) (SourceMirrorWorkspaceCleanupResult, error) {
	logger := s.Logger
	if logger == nil {
		logger = slog.Default()
	}
	root := strings.TrimSpace(s.WorkDir)
	if root == "" {
		root = filepath.Join(os.TempDir(), "meshx-kb-source-mirrors")
	}
	entries, err := os.ReadDir(root)
	if os.IsNotExist(err) {
		return SourceMirrorWorkspaceCleanupResult{}, nil
	}
	if err != nil {
		return SourceMirrorWorkspaceCleanupResult{}, fmt.Errorf("read source mirror workspace root: %w", err)
	}
	staleAge := SourceMirrorLeaseDuration(s.Timeout)
	result := SourceMirrorWorkspaceCleanupResult{}
	for _, entry := range entries {
		if err := ctx.Err(); err != nil {
			return result, err
		}
		if strings.HasPrefix(entry.Name(), sourceMirrorWorkspaceDeletingPrefix) {
			path := filepath.Join(root, entry.Name())
			removed, err := removeSourceMirrorQuarantineEntry(entry, path)
			if err != nil {
				recordSourceMirrorCleanupFailure(ctx, logger, &result)
			} else if removed {
				result.Removed++
			}
			continue
		}
		if !strings.HasPrefix(entry.Name(), sourceMirrorWorkspacePrefix) || entry.Type()&os.ModeSymlink != 0 {
			continue
		}
		info, err := entry.Info()
		if os.IsNotExist(err) {
			continue
		}
		if err != nil {
			recordSourceMirrorCleanupFailure(ctx, logger, &result)
			continue
		}
		if !info.IsDir() {
			continue
		}
		path := filepath.Join(root, entry.Name())
		_, lockStatErr := os.Lstat(filepath.Join(path, sourceMirrorWorkspaceLockName))
		legacy := os.IsNotExist(lockStatErr)
		if lockStatErr != nil && !legacy {
			recordSourceMirrorCleanupFailure(ctx, logger, &result)
			continue
		}
		candidateAge := staleAge
		if legacy && candidateAge < legacySourceMirrorWorkspaceStaleAge {
			candidateAge = legacySourceMirrorWorkspaceStaleAge
		}
		if info.ModTime().After(now) || now.Sub(info.ModTime()) < candidateAge {
			result.SkippedRecent++
			continue
		}
		lifecycleLock, acquired, err := tryLockSourceMirrorFile(filepath.Join(root, sourceMirrorLifecycleLockSuffix))
		if err != nil {
			recordSourceMirrorCleanupFailure(ctx, logger, &result)
			continue
		}
		if !acquired {
			continue
		}
		lock, acquired, err := tryLockSourceMirrorFile(filepath.Join(path, sourceMirrorWorkspaceLockName))
		if os.IsNotExist(err) {
			_ = releaseSourceMirrorWorkspaceLock(lifecycleLock)
			continue
		}
		if err != nil {
			err = errors.Join(err, releaseSourceMirrorWorkspaceLock(lifecycleLock))
			recordSourceMirrorCleanupFailure(ctx, logger, &result)
			continue
		}
		if !acquired {
			if err := releaseSourceMirrorWorkspaceLock(lifecycleLock); err != nil {
				recordSourceMirrorCleanupFailure(ctx, logger, &result)
				continue
			}
			result.SkippedActive++
			continue
		}
		lockedInfo, err := os.Stat(path)
		if err != nil || !os.SameFile(info, lockedInfo) {
			returnErr := errors.Join(releaseSourceMirrorWorkspaceLock(lock), releaseSourceMirrorWorkspaceLock(lifecycleLock))
			if os.IsNotExist(err) && returnErr == nil {
				continue
			}
			if err == nil {
				err = fmt.Errorf("source mirror workspace changed while acquiring lock")
			}
			err = errors.Join(err, returnErr)
			recordSourceMirrorCleanupFailure(ctx, logger, &result)
			continue
		}
		_, removed, err := removeSourceMirrorWorkspace(path, lock, lifecycleLock)
		if err != nil {
			recordSourceMirrorCleanupFailure(ctx, logger, &result)
			continue
		}
		if removed {
			result.Removed++
		}
	}
	return result, nil
}

func recordSourceMirrorCleanupFailure(ctx context.Context, logger *slog.Logger, result *SourceMirrorWorkspaceCleanupResult) {
	logger.WarnContext(ctx, "stale source mirror workspace cleanup failed", "error_class", "workspace_remove_failed")
	result.Failed++
}

func removeSourceMirrorWorkspace(path string, workspaceLock, lifecycleLock *os.File) (cleanupPath string, removed bool, returnErr error) {
	cleanupPath = path
	basename := filepath.Base(path)
	if !strings.HasPrefix(basename, sourceMirrorWorkspacePrefix) || strings.HasPrefix(basename, sourceMirrorWorkspaceDeletingPrefix) {
		return cleanupPath, false, errors.Join(
			fmt.Errorf("invalid source mirror workspace path %q", path),
			releaseSourceMirrorWorkspaceLock(workspaceLock),
			releaseSourceMirrorWorkspaceLock(lifecycleLock),
		)
	}
	tombstone := filepath.Join(filepath.Dir(path), sourceMirrorWorkspaceDeletingPrefix+strings.TrimPrefix(basename, sourceMirrorWorkspacePrefix))
	if _, err := os.Lstat(tombstone); err == nil {
		return cleanupPath, false, errors.Join(
			fmt.Errorf("source mirror workspace tombstone already exists %q", tombstone),
			releaseSourceMirrorWorkspaceLock(workspaceLock),
			releaseSourceMirrorWorkspaceLock(lifecycleLock),
		)
	} else if !os.IsNotExist(err) {
		return cleanupPath, false, errors.Join(
			fmt.Errorf("stat source mirror workspace tombstone: %w", err),
			releaseSourceMirrorWorkspaceLock(workspaceLock),
			releaseSourceMirrorWorkspaceLock(lifecycleLock),
		)
	}
	if err := os.Rename(path, tombstone); err != nil {
		releaseErr := errors.Join(releaseSourceMirrorWorkspaceLock(workspaceLock), releaseSourceMirrorWorkspaceLock(lifecycleLock))
		if os.IsNotExist(err) {
			return cleanupPath, false, releaseErr
		}
		return cleanupPath, false, errors.Join(fmt.Errorf("quarantine source mirror workspace: %w", err), releaseErr)
	}
	cleanupPath = tombstone
	// The root lock makes the path transition atomic across creators and cleaners.
	// Release both file descriptors before RemoveAll so NFS cannot retain .nfs files.
	if err := errors.Join(releaseSourceMirrorWorkspaceLock(workspaceLock), releaseSourceMirrorWorkspaceLock(lifecycleLock)); err != nil {
		return cleanupPath, false, fmt.Errorf("release source mirror workspace cleanup locks: %w", err)
	}
	removed, err := removeSourceMirrorQuarantine(cleanupPath)
	if err != nil {
		return cleanupPath, false, fmt.Errorf("remove source mirror workspace: %w", err)
	}
	return cleanupPath, removed, nil
}

func removeSourceMirrorQuarantineEntry(entry os.DirEntry, path string) (bool, error) {
	info, err := entry.Info()
	if os.IsNotExist(err) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
		return false, nil
	}
	return removeSourceMirrorQuarantine(path)
}

func removeSourceMirrorQuarantine(path string) (bool, error) {
	if _, err := os.Lstat(path); os.IsNotExist(err) {
		return false, nil
	} else if err != nil {
		return false, err
	}
	if err := os.RemoveAll(path); err != nil {
		return false, err
	}
	return true, nil
}
