package tenants

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/observability/vm-migration/internal/printer"
)

// DedicatedK8sOptions carries inputs to k8s dedicated discovery.
type DedicatedK8sOptions struct {
	Output string
}

// DedicatedS3Options carries inputs to S3-archive dedicated discovery.
type DedicatedS3Options struct {
	S3Bucket string
	MaxAge   string
	Output   string
}

// DefaultMaxAge is the --max-age default.
//
// Archive files older than this are skipped during S3 discovery.
const DefaultMaxAge = "90d"

// kubectlShellLister is the production KubectlLister.
//
// Shells out to `kubectl get clusters`. Tests inject fakes.
type kubectlShellLister struct{}

func (k *kubectlShellLister) List(ctx context.Context, selector string) ([]byte, error) {
	out, err := runCLI(ctx, KubectlExecutable,
		[]string{"get", "clusters", "-A", "-l", selector, "-o", "json"},
		"kubectl get clusters")
	return []byte(out), err
}

// NewKubectlShellLister returns the production KubectlLister (shells out to
// kubectl).
func NewKubectlShellLister() KubectlLister { return &kubectlShellLister{} }

// RunDedicatedK8s discovers dedicated tenants from live Kubernetes clusters
// and writes the result.
func RunDedicatedK8s(ctx context.Context, opts DedicatedK8sOptions, k8sLister KubectlLister, stdout, stderr io.Writer) error {
	printer.Printf(stderr, "discovering from kubernetes...")
	entries, err := discoverFromKubernetes(ctx, k8sLister, DedicatedBizTypes)
	if err != nil {
		return err
	}
	printer.Printf(stderr, "  kubernetes: %d tenant(s)", len(entries))
	n, err := writeEntries(opts.Output, entries, stdout)
	if err != nil {
		return err
	}
	printer.Printf(stderr, "wrote %d tenants to %s", n, destName(opts.Output))
	return nil
}

// RunDedicatedS3 discovers dedicated tenants from the S3 archive — every
// cluster.yaml under the archived/<bizType>/ prefixes, bulk-downloaded and
// kept when modified within MaxAge — and writes the result.
func RunDedicatedS3(
	ctx context.Context,
	opts DedicatedS3Options,
	s3Lister S3ArchiveLister,
	s3Downloader S3ArchiveDownloader,
	stdout, stderr io.Writer,
) error {
	maxAge, err := ParseMaxAge(opts.MaxAge)
	if err != nil {
		return err
	}
	printer.Printf(stderr, "discovering from s3 (bucket=%s, max-age=%s)...", opts.S3Bucket, formatMaxAge(maxAge))
	entries, err := discoverFromS3(ctx, s3Lister, s3Downloader, opts.S3Bucket, DedicatedBizTypes, maxAge, stderr)
	if err != nil {
		return err
	}
	printer.Printf(stderr, "  s3: %d tenant(s)", len(entries))
	n, err := writeEntries(opts.Output, entries, stdout)
	if err != nil {
		return err
	}
	printer.Printf(stderr, "wrote %d tenants to %s", n, destName(opts.Output))
	return nil
}

// writeEntries dedups by tenant (archive sources win over k8s), sorts, and
// writes the tenants artifact.
//
// Returns the merged count; errors when entries is empty.
func writeEntries(output string, entries []Entry, stdout io.Writer) (int, error) {
	if len(entries) == 0 {
		return 0, errors.New("no tenants found")
	}
	merged := mergeEntries(entries)
	sortByTenant(merged)
	if err := WriteEntriesFile(output, merged, stdout); err != nil {
		return 0, err
	}
	return len(merged), nil
}

func discoverFromKubernetes(ctx context.Context, lister KubectlLister, bizTypes []string) ([]Entry, error) {
	payload, err := lister.List(ctx, dedicatedSelector(bizTypes))
	if err != nil {
		return nil, err
	}
	return EntriesFromKubectlJSON(payload, bizTypes)
}

func discoverFromS3(
	ctx context.Context,
	lister S3ArchiveLister,
	downloader S3ArchiveDownloader,
	bucket string,
	bizTypes []string,
	maxAge time.Duration,
	stderr io.Writer,
) ([]Entry, error) {
	cutoff := time.Now().Add(-maxAge)
	tmpRoot, err := os.MkdirTemp("", "dedicated-tenants-s3-")
	if err != nil {
		return nil, err
	}
	defer os.RemoveAll(tmpRoot)

	var all []Entry
	for _, bizType := range bizTypes {
		localDir := filepath.Join(tmpRoot, bizType)
		if err := os.MkdirAll(localDir, 0o755); err != nil {
			return nil, err
		}

		objects, err := lister.List(ctx, bucket, bizType)
		if err != nil {
			return nil, err
		}
		validRel := relPathsSinceCutoff(bizType, objects, cutoff)
		printer.Printf(stderr, "  %s: %d/%d cluster.yaml within %s", bizType, len(validRel), len(objects), formatMaxAge(maxAge))

		started := time.Now()
		printer.Printf(stderr, "  %s: downloading archive (%d files)...", bizType, len(objects))
		if err := downloader.Download(ctx, bucket, bizType, localDir); err != nil {
			return nil, err
		}
		printer.Printf(stderr, "  %s: downloaded %d cluster.yaml in %.1fs", bizType, len(objects), time.Since(started).Seconds())

		entries, err := EntriesFromArchiveDir(localDir, bizTypes, SourceS3, validRel)
		if err != nil {
			return nil, err
		}
		printer.Printf(stderr, "  %s: parsed %d tenant(s)", bizType, len(entries))
		all = append(all, entries...)
	}
	return all, nil
}

// ParseMaxAge parses "<N>d" (e.g., "30d") into a duration of N days.
func ParseMaxAge(text string) (time.Duration, error) {
	text = strings.TrimSpace(strings.ToLower(text))
	if !strings.HasSuffix(text, "d") {
		return 0, fmt.Errorf("invalid duration %q: expected format like \"30d\"", text)
	}
	nStr := text[:len(text)-1]
	n, err := strconv.Atoi(nStr)
	if err != nil || n <= 0 {
		return 0, fmt.Errorf("invalid duration %q: expected a positive integer like \"30d\"", text)
	}
	return time.Duration(n) * 24 * time.Hour, nil
}

// formatMaxAge renders a max-age duration in its "<N>d" flag form.
//
// ParseMaxAge accepts whole days only, so the round trip is lossless.
func formatMaxAge(maxAge time.Duration) string {
	return fmt.Sprintf("%dd", int(maxAge.Hours()/24))
}
