package detector

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"strings"
	"time"

	"github.com/step-security/dev-machine-guard/internal/executor"
	"github.com/step-security/dev-machine-guard/internal/model"
	"github.com/step-security/dev-machine-guard/internal/progress"
	"github.com/step-security/dev-machine-guard/internal/tcc"
)

// PythonScanner performs enterprise-mode Python scanning (raw output, base64 encoded).
type PythonScanner struct {
	exec executor.Executor
	log  *progress.Logger
	// ProgressHook, when non-nil, is invoked from inside ScanGlobalPackages
	// with a short human-readable detail string ("scanning pip3", ...).
	// Telemetry plumbs this into PhaseTracker.UpdateDetail so heartbeats
	// surface mid-phase progress.
	ProgressHook func(detail string)
}

func NewPythonScanner(exec executor.Executor, log *progress.Logger) *PythonScanner {
	return &PythonScanner{exec: exec, log: log}
}

func (s *PythonScanner) emitProgress(detail string) {
	if s.ProgressHook != nil {
		s.ProgressHook(detail)
	}
}

type pythonScanSpec struct {
	binary     string
	name       string
	versionCmd string
	listArgs   []string
}

var pythonScanSpecs = []pythonScanSpec{
	{"pip3", "pip", "--version", []string{"list", "--format", "json"}},
	{"conda", "conda", "--version", []string{"list", "--json"}},
	{"uv", "uv", "--version", []string{"pip", "list", "--format", "json"}},
}

// ScanGlobalPackages runs pip3/conda/uv list and returns raw base64-encoded results.
func (s *PythonScanner) ScanGlobalPackages(ctx context.Context) []model.PythonScanResult {
	var results []model.PythonScanResult

	for _, spec := range pythonScanSpecs {
		binPath, err := s.exec.LookPath(spec.binary)
		if err != nil {
			continue
		}
		if s.exec.IsAppleCLTStub(ctx, binPath) {
			// On a Mac without Command Line Tools, /usr/bin/pip3 etc. are install-
			// prompt shims — invoking `list` would pop a GUI dialog for the user.
			continue
		}

		s.emitProgress("scanning " + spec.name)
		s.log.Progress("  Checking %s global packages...", spec.name)
		version := s.getVersion(ctx, spec.binary, spec.versionCmd)

		start := time.Now()
		args := spec.listArgs
		stdout, stderr, exitCode, _ := s.exec.RunWithTimeout(ctx, 60*time.Second, spec.binary, args...)
		duration := time.Since(start).Milliseconds()

		errMsg := ""
		if exitCode != 0 {
			errMsg = spec.binary + " list command failed"
			s.log.Warn("%s list failed (exit_code=%d, %dms) — results may be incomplete", spec.binary, exitCode, duration)
		}
		s.log.Debug("%s global scan: version=%s binary=%s exit_code=%d stdout_bytes=%d duration=%dms", spec.name, version, binPath, exitCode, len(stdout), duration)

		results = append(results, model.PythonScanResult{
			PackageManager:  spec.name,
			PMVersion:       version,
			BinaryPath:      binPath,
			RawStdoutBase64: base64.StdEncoding.EncodeToString([]byte(stdout)),
			RawStderrBase64: base64.StdEncoding.EncodeToString([]byte(stderr)),
			Error:           errMsg,
			ExitCode:        exitCode,
			ScanDurationMs:  duration,
		})
	}

	return results
}

// ScanGlobalPackagesFromDisk inventories global / user site-packages by
// reading install metadata on disk (no pip/conda/uv subprocess) and returns
// the result in the same PythonScanResult shape as ScanGlobalPackages: the
// package list is JSON-encoded ([{"name","version"},...]) into RawStdoutBase64
// so the existing backend decoder needs no change. Returns nil when no global
// site-packages roots exist on the host.
func (s *PythonScanner) ScanGlobalPackagesFromDisk(skipper *tcc.Skipper) []model.PythonScanResult {
	roots := PythonGlobalRoots(s.exec)
	if len(roots) == 0 {
		s.log.Debug("python global disk scan: no site-packages roots found")
		return nil
	}

	s.emitProgress("scanning global site-packages")
	s.log.Progress("  Scanning global Python site-packages on disk...")

	start := time.Now()
	dist := NewPythonDistDetector(s.exec).WithLogger(s.log).WithSkipper(skipper)
	pkgs := dist.ScanRoots(roots)
	duration := time.Since(start).Milliseconds()

	type pipEntry struct {
		Name    string `json:"name"`
		Version string `json:"version"`
	}
	entries := make([]pipEntry, len(pkgs))
	for i, p := range pkgs {
		entries[i] = pipEntry{Name: p.Name, Version: p.Version}
	}
	raw, _ := json.Marshal(entries)
	s.log.Debug("python global disk scan: roots=%d packages=%d duration=%dms", len(roots), len(pkgs), duration)

	return []model.PythonScanResult{{
		PackageManager:  "pip",
		RawStdoutBase64: base64.StdEncoding.EncodeToString(raw),
		ExitCode:        0,
		ScanDurationMs:  duration,
	}}
}

func (s *PythonScanner) getVersion(ctx context.Context, binary, versionCmd string) string {
	stdout, _, _, err := s.exec.RunWithTimeout(ctx, 10*time.Second, binary, versionCmd)
	if err != nil {
		return "unknown"
	}
	v := strings.TrimSpace(stdout)
	if v == "" {
		return "unknown"
	}
	return parsePythonVersion(binary, v)
}
