package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"sort"
	"strings"

	"github.com/spf13/cobra"

	mcrv1 "git.wntrmute.dev/mc/mcr/gen/mcr/v1"
)

func purgeCmd() *cobra.Command {
	var (
		keep    int
		repo    string
		dryRun  bool
		trigGC  bool
	)

	cmd := &cobra.Command{
		Use:   "purge",
		Short: "Remove old image tags, keeping the last N per repository",
		Long: `Purge removes old image tags from repositories based on a retention
policy. The 'latest' tag is always kept. For each repository, tags are
sorted by updated_at (most recent first) and only the --keep most recent
are retained. Remaining tags' manifests are deleted, cascading to their
tag references. Run with --gc to trigger garbage collection afterward.`,
		RunE: func(_ *cobra.Command, _ []string) error {
			return runPurge(keep, repo, dryRun, trigGC)
		},
	}

	cmd.Flags().IntVar(&keep, "keep", 3, "number of tags to keep per repo (excluding latest)")
	cmd.Flags().StringVar(&repo, "repo", "", "limit to repositories matching this glob pattern")
	cmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be deleted without deleting")
	cmd.Flags().BoolVar(&trigGC, "gc", false, "trigger garbage collection after purging")

	return cmd
}

// tagWithTime is a tag with its timestamp for sorting.
type tagWithTime struct {
	Name      string
	Digest    string
	UpdatedAt string
}

func runPurge(keep int, repoPattern string, dryRun, trigGC bool) error {
	if keep < 0 {
		return fmt.Errorf("--keep must be non-negative")
	}

	// List repositories.
	repos, err := listRepos()
	if err != nil {
		return err
	}

	// Filter by pattern if given.
	if repoPattern != "" {
		var filtered []string
		for _, r := range repos {
			matched, _ := path.Match(repoPattern, r)
			if matched {
				filtered = append(filtered, r)
			}
		}
		repos = filtered
	}

	if len(repos) == 0 {
		_, _ = fmt.Fprintln(os.Stdout, "No repositories matched.")
		return nil
	}

	totalDeleted := 0

	for _, repoName := range repos {
		tags, err := listTagsWithTime(repoName)
		if err != nil {
			_, _ = fmt.Fprintf(os.Stderr, "warning: skipping %s: %v\n", repoName, err)
			continue
		}

		// Separate latest from versioned tags.
		var latest *tagWithTime
		var versioned []tagWithTime
		for _, t := range tags {
			if t.Name == "latest" {
				lt := t
				latest = &lt
			} else {
				versioned = append(versioned, t)
			}
		}

		// Sort by updated_at descending (most recent first).
		sort.Slice(versioned, func(i, j int) bool {
			return versioned[i].UpdatedAt > versioned[j].UpdatedAt
		})

		// Determine which tags to delete.
		if len(versioned) <= keep {
			continue // nothing to purge
		}

		toDelete := versioned[keep:]

		// Build set of digests that are still referenced by kept tags.
		keptDigests := make(map[string]bool)
		if latest != nil {
			keptDigests[latest.Digest] = true
		}
		for _, t := range versioned[:keep] {
			keptDigests[t.Digest] = true
		}

		for _, t := range toDelete {
			if keptDigests[t.Digest] {
				// Manifest is shared with a kept tag — skip deletion.
				if dryRun {
					_, _ = fmt.Fprintf(os.Stdout, "[skip]   %s:%s (manifest shared with kept tag)\n", repoName, t.Name)
				}
				continue
			}

			if dryRun {
				_, _ = fmt.Fprintf(os.Stdout, "[delete] %s:%s (digest: %s, updated: %s)\n", repoName, t.Name, t.Digest[:12], t.UpdatedAt)
			} else {
				if err := deleteManifest(repoName, t.Digest); err != nil {
					_, _ = fmt.Fprintf(os.Stderr, "warning: failed to delete %s:%s: %v\n", repoName, t.Name, err)
					continue
				}
				_, _ = fmt.Fprintf(os.Stdout, "deleted %s:%s\n", repoName, t.Name)
			}
			totalDeleted++
		}
	}

	if dryRun {
		_, _ = fmt.Fprintf(os.Stdout, "\nDry run: %d manifests would be deleted.\n", totalDeleted)
	} else {
		_, _ = fmt.Fprintf(os.Stdout, "\nPurged %d manifests.\n", totalDeleted)
	}

	// Trigger GC if requested and not a dry run.
	if trigGC && !dryRun && totalDeleted > 0 {
		_, _ = fmt.Fprintln(os.Stdout, "Triggering garbage collection...")
		if err := triggerGC(); err != nil {
			return fmt.Errorf("gc: %w", err)
		}
		_, _ = fmt.Fprintln(os.Stdout, "Garbage collection started.")
	}

	return nil
}

// listRepos returns all repository names.
func listRepos() ([]string, error) {
	if client.useGRPC() {
		resp, err := client.registry.ListRepositories(context.Background(), &mcrv1.ListRepositoriesRequest{})
		if err != nil {
			return nil, fmt.Errorf("list repositories: %w", err)
		}
		var names []string
		for _, r := range resp.GetRepositories() {
			names = append(names, r.GetName())
		}
		return names, nil
	}

	data, err := client.restDo("GET", "/v1/repositories", nil)
	if err != nil {
		return nil, fmt.Errorf("list repositories: %w", err)
	}

	var repos []struct {
		Name string `json:"name"`
	}
	if err := json.Unmarshal(data, &repos); err != nil {
		return nil, err
	}

	var names []string
	for _, r := range repos {
		names = append(names, r.Name)
	}
	return names, nil
}

// listTagsWithTime returns tags with timestamps for a repository.
func listTagsWithTime(repoName string) ([]tagWithTime, error) {
	if client.useGRPC() {
		resp, err := client.registry.GetRepository(context.Background(), &mcrv1.GetRepositoryRequest{Name: repoName})
		if err != nil {
			return nil, fmt.Errorf("get repository: %w", err)
		}
		var tags []tagWithTime
		for _, t := range resp.GetTags() {
			tags = append(tags, tagWithTime{
				Name:      t.GetName(),
				Digest:    t.GetDigest(),
				UpdatedAt: t.GetUpdatedAt(),
			})
		}
		return tags, nil
	}

	data, err := client.restDo("GET", "/v1/repositories/"+repoName, nil)
	if err != nil {
		return nil, fmt.Errorf("get repository: %w", err)
	}

	var detail struct {
		Tags []struct {
			Name      string `json:"name"`
			Digest    string `json:"digest"`
			UpdatedAt string `json:"updated_at"`
		} `json:"tags"`
	}
	if err := json.Unmarshal(data, &detail); err != nil {
		return nil, err
	}

	var tags []tagWithTime
	for _, t := range detail.Tags {
		tags = append(tags, tagWithTime{
			Name:      t.Name,
			Digest:    t.Digest,
			UpdatedAt: t.UpdatedAt,
		})
	}
	return tags, nil
}

// deleteManifest deletes a manifest by digest via the OCI API.
func deleteManifest(repoName, digest string) error {
	if client.useGRPC() {
		// No gRPC RPC for OCI manifest delete — fall back to REST.
		// The OCI Distribution spec uses DELETE /v2/{name}/manifests/{reference}.
	}

	// URL-encode the repo name for path segments with slashes.
	_, err := client.restDo("DELETE", "/v2/"+repoName+"/manifests/"+digest, nil)
	return err
}

// triggerGC starts garbage collection.
func triggerGC() error {
	if client.useGRPC() {
		_, err := client.registry.GarbageCollect(context.Background(), &mcrv1.GarbageCollectRequest{})
		return err
	}

	_, err := client.restDo("POST", "/v1/gc", nil)
	return err
}

// formatRepoName truncates long repo names for display.
func formatRepoName(name string, maxLen int) string {
	if len(name) <= maxLen {
		return name
	}
	parts := strings.Split(name, "/")
	if len(parts) > 1 {
		return "..." + parts[len(parts)-1]
	}
	return name[:maxLen-3] + "..."
}
