package crd

import (
	"context"
	"fmt"
	"log"

	"github.com/tidbcloud/controlplane9/internal/config"
)

// Options configures a CRD installation run.
type Options struct {
	// Fresh forces re-dump even if cache is fresh.
	Fresh bool
	// SeedFlag is the --seed flag value ("", "crds-only", "config-crs", "full").
	SeedFlag string
	// Services is the list of controller services with CRD mappings.
	Services []string
	// CRDMappings maps service name → CRD API group mapping.
	CRDMappings map[string]config.CRDMapping
	// Secrets maps service name → secret source (from cp9.local.yaml).
	Secrets map[string]config.SecretSource
	// Namespace is the target namespace for CRs and secrets.
	Namespace string
	// SeedConfig from cp9.yaml crds.seed section.
	SeedConfig config.CRDSeedConfig
	// StateDir is the cp9 state directory for caching.
	StateDir string
	// Dumper performs kubectl operations against staging. If nil, a real dumper is used.
	Dumper *Dumper
	// Applier applies YAML to the local cluster. If nil, a real applier is used.
	Applier *Applier
}

// Run dumps CRDs from staging, applies them locally, seeds CRs, and injects secrets.
func Run(ctx context.Context, opts *Options) *Result {
	result := &Result{}

	applier := opts.Applier
	if applier == nil {
		applier = &Applier{}
	}

	dumper := opts.Dumper
	if dumper == nil {
		dumper = &Dumper{}
	}

	for _, svc := range opts.Services {
		mapping, ok := opts.CRDMappings[svc]
		if !ok {
			result.Services = append(result.Services, ServiceResult{
				Service: svc, Status: "skipped",
			})
			continue
		}

		if mapping.Exclude {
			log.Printf("crd: skipping excluded service %s", svc)
			result.Services = append(result.Services, ServiceResult{
				Service: svc, Status: "skipped",
			})
			continue
		}

		svcResult := runService(ctx, svc, mapping, opts, dumper, applier)
		result.Services = append(result.Services, svcResult)

		if svcResult.Status == "failed" {
			return result
		}
	}

	// Process secrets.
	if len(opts.Secrets) > 0 {
		namespace := opts.Namespace
		if namespace == "" {
			namespace = "default"
		}
		result.Secrets = ProcessSecrets(ctx, applier, opts.Secrets, namespace)
	}

	return result
}

func runService(ctx context.Context, svc string, mapping config.CRDMapping, opts *Options, dumper *Dumper, applier *Applier) ServiceResult {
	// List CRDs matching this service's API groups from staging.
	crdNames, err := dumper.ListCRDs(ctx, mapping.APIGroups)
	if err != nil {
		return ServiceResult{Service: svc, Status: "failed",
			Error: fmt.Sprintf("list CRDs: %v", err)}
	}

	if len(crdNames) == 0 {
		log.Printf("crd: no CRDs found for %s (groups: %v)", svc, mapping.APIGroups)
		return ServiceResult{Service: svc, Status: "skipped"}
	}

	usedCache := false
	crdCount := 0

	// Dump and apply each CRD.
	for _, crdName := range crdNames {
		cachePath := CachedCRDPath(opts.StateDir, svc, crdName)

		var data []byte
		if !opts.Fresh && IsCacheFresh(cachePath) {
			cached, readErr := ReadCacheFile(cachePath)
			if readErr == nil {
				data = cached
				usedCache = true
				log.Printf("crd: using cached CRD %s for %s", crdName, svc)
			}
		}

		if data == nil {
			data, err = dumper.DumpCRD(ctx, crdName)
			if err != nil {
				return ServiceResult{Service: svc, Status: "failed",
					Error: fmt.Sprintf("dump CRD %s: %v", crdName, err)}
			}
			if writeErr := WriteCacheFile(cachePath, data); writeErr != nil {
				log.Printf("crd: warning: failed to cache CRD %s: %v", crdName, writeErr)
			}
		}

		if err := applier.Apply(ctx, data); err != nil {
			return ServiceResult{Service: svc, Status: "failed",
				Error: fmt.Sprintf("apply CRD %s: %v", crdName, err)}
		}
		crdCount++
	}

	// Seed CRs if requested.
	crCount := 0
	seedMode := resolveCRDSeedMode(opts.SeedFlag, opts.SeedConfig, svc)
	if seedMode != config.CRDSeedCRDsOnly {
		count, seedErr := seedCRs(ctx, svc, crdNames, seedMode, opts, dumper, applier)
		if seedErr != nil {
			return ServiceResult{Service: svc, Status: "failed",
				Error: fmt.Sprintf("seed CRs: %v", seedErr)}
		}
		crCount = count
	}

	status := "ok"
	if usedCache {
		status = "cached"
	}
	return ServiceResult{Service: svc, Status: status, CRDCount: crdCount, CRCount: crCount}
}

func seedCRs(
	ctx context.Context,
	svc string,
	crdNames []string,
	mode config.CRDSeedMode,
	opts *Options,
	dumper *Dumper,
	applier *Applier,
) (int, error) {
	// Determine which CR kinds to seed.
	var resources []string
	if override, ok := opts.SeedConfig.Overrides[svc]; ok && len(override.Kinds) > 0 {
		resources = override.Kinds
	} else if mode == config.CRDSeedFull {
		// Seed all CRD resources.
		for _, name := range crdNames {
			resources = append(resources, crdNameToResource(name))
		}
	}

	count := 0
	for _, resource := range resources {
		cachePath := CachedCRPath(opts.StateDir, svc, resource)

		var data []byte
		if !opts.Fresh && IsCacheFresh(cachePath) {
			cached, readErr := ReadCacheFile(cachePath)
			if readErr == nil {
				data = cached
				log.Printf("crd: using cached CRs for %s/%s", svc, resource)
			}
		}

		if data == nil {
			var err error
			data, err = dumper.DumpCRs(ctx, resource, "")
			if err != nil {
				return count, fmt.Errorf("dump CRs %s: %w", resource, err)
			}
			if writeErr := WriteCacheFile(cachePath, data); writeErr != nil {
				log.Printf("crd: warning: failed to cache CRs %s: %v", resource, writeErr)
			}
		}

		if err := applier.Apply(ctx, data); err != nil {
			return count, fmt.Errorf("apply CRs %s: %w", resource, err)
		}
		count++
	}

	return count, nil
}

// crdNameToResource converts a CRD name (e.g. "certificates.cert-manager.io") to its
// plural resource name (e.g. "certificates").
func crdNameToResource(crdName string) string {
	parts := splitCRDName(crdName)
	if len(parts) > 0 {
		return parts[0]
	}
	return crdName
}

// splitCRDName splits "resource.group.domain" into ["resource", "group.domain"].
func splitCRDName(name string) []string {
	idx := indexByte(name, '.')
	if idx < 0 {
		return []string{name}
	}
	return []string{name[:idx], name[idx+1:]}
}

func indexByte(s string, c byte) int {
	for i := range len(s) {
		if s[i] == c {
			return i
		}
	}
	return -1
}

// resolveCRDSeedMode determines the effective CRD seed mode.
func resolveCRDSeedMode(seedFlag string, seedCfg config.CRDSeedConfig, service string) config.CRDSeedMode {
	// Flag overrides config.
	switch seedFlag {
	case "crds-only":
		return config.CRDSeedCRDsOnly
	case "config-crs":
		return config.CRDSeedConfigCRs
	case "full":
		return config.CRDSeedFull
	}

	// Check per-service override.
	if _, ok := seedCfg.Overrides[service]; ok {
		return config.CRDSeedConfigCRs
	}

	// Use configured default.
	if seedCfg.Default != "" {
		return seedCfg.Default
	}

	return config.CRDSeedCRDsOnly
}
