package doctor

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

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

// StubInfo describes a generated stub for drift checking.
type StubInfo struct {
	Service     string
	Kind        stub.Kind
	InputFiles  []string // original input file paths from generation manifest
	OutputDir   string
	ManifestErr string // non-empty if manifest was present but unreadable
}

// CheckStubDrift compares current proto/swagger definitions against what stubs
// were generated from. Flags services where definitions changed since last generation.
func CheckStubDrift(r *Result, stubs []StubInfo) {
	if len(stubs) == 0 {
		r.Pass("stub-drift", "no stubs to check")
		return
	}

	var drifted []string
	var checked int

	for _, s := range stubs {
		if s.ManifestErr != "" {
			drifted = append(drifted, fmt.Sprintf("%s (%s)", s.Service, s.ManifestErr))
			checked++
			continue
		}
		if s.Kind == stub.KindPermissive || len(s.InputFiles) == 0 {
			continue // permissive stubs have no source definition to drift
		}

		// Check that all source files still exist.
		allExist := true
		for _, f := range s.InputFiles {
			if _, err := os.Stat(f); err != nil {
				allExist = false
				break
			}
		}
		if !allExist {
			drifted = append(drifted, fmt.Sprintf("%s (source files missing)", s.Service))
			checked++
			continue
		}

		currentKey, err := stub.ComputeCacheKey(s.InputFiles)
		if err != nil {
			drifted = append(drifted, fmt.Sprintf("%s (cannot compute hash: %v)", s.Service, err))
			checked++
			continue
		}

		if !stub.CheckCache(s.OutputDir, currentKey) {
			drifted = append(drifted, s.Service)
		}
		checked++
	}

	if len(drifted) > 0 {
		r.Warn("stub-drift", fmt.Sprintf("%d stub(s) have drifted from their source definitions", len(drifted)),
			strings.Join(drifted, ", ")+"\nrerun cp9 up after refreshing the stub source inputs")
		return
	}
	if checked == 0 {
		r.Pass("stub-drift", "no stubs with source definitions to check")
		return
	}
	r.Pass("stub-drift", fmt.Sprintf("%d stub(s) up to date", checked))
}

// CheckRecordingStubContracts verifies that services which need Premium
// Reserved recording behavior are no longer backed by generic permissive stubs.
func CheckRecordingStubContracts(r *Result, stubs []StubInfo) {
	required := map[string]stub.Kind{
		"metering":    stub.KindMeteringRecorder,
		"billing-svc": stub.KindBillingRecorder,
	}

	var checked int
	var missing []string
	for _, s := range stubs {
		wantKind, ok := required[s.Service]
		if !ok {
			continue
		}
		checked++
		if s.Kind != wantKind {
			missing = append(missing, fmt.Sprintf("%s (found %s, want %s)", s.Service, s.Kind, wantKind))
		}
	}

	switch {
	case len(missing) > 0:
		r.Warn("recording-stubs",
			fmt.Sprintf("%d recording stub contract(s) not materialized", len(missing)),
			strings.Join(missing, ", "))
	case checked == 0:
		r.Pass("recording-stubs", "no metering or billing recording stubs to check")
	default:
		r.Pass("recording-stubs", fmt.Sprintf("%d recording stub contract(s) satisfied", checked))
	}
}

// DiscoverStubs scans a state directory for generated stubs and returns StubInfo entries.
// It reads the generation manifest from each stub directory for reliable kind and
// input file detection.
func DiscoverStubs(stateDir string) []StubInfo {
	stubsDir := filepath.Join(stateDir, "stubs")
	entries, err := os.ReadDir(stubsDir)
	if err != nil {
		return nil
	}

	var stubs []StubInfo
	for _, e := range entries {
		if !e.IsDir() {
			continue
		}
		svcDir := filepath.Join(stubsDir, e.Name())
		si := StubInfo{
			Service:   e.Name(),
			OutputDir: svcDir,
		}

		// Read the generation manifest for reliable kind/input detection.
		manifest, mErr := stub.ReadManifest(svcDir)
		switch {
		case mErr == nil:
			si.Kind = manifest.Kind
			si.InputFiles = manifest.InputFiles
		case errors.Is(mErr, stub.ErrNoManifest):
			// Fallback for stubs generated before manifest support.
			si.Kind = detectStubKind(svcDir)
		default:
			// Manifest exists but is corrupted/unreadable — flag it.
			si.Kind = detectStubKind(svcDir)
			si.ManifestErr = fmt.Sprintf("manifest unreadable: %v", mErr)
		}

		stubs = append(stubs, si)
	}
	return stubs
}

// detectStubKind detects stub kind by checking the definitions/ subtree.
// This is a fallback for stubs generated before manifest support.
func detectStubKind(svcDir string) stub.Kind {
	defDir := filepath.Join(svcDir, "definitions")
	switch {
	case hasGlobMatch(defDir, "*.proto"):
		return stub.KindProto
	case fileExists(filepath.Join(defDir, "swagger.json")):
		return stub.KindSwagger
	default:
		return stub.KindPermissive
	}
}

func hasGlobMatch(dir, pattern string) bool {
	matches, _ := filepath.Glob(filepath.Join(dir, pattern))
	return len(matches) > 0
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}
