package cli

import (
	"fmt"
	"path/filepath"
	"time"

	"github.com/dustin/go-humanize"
	"github.com/spf13/afero"
	"github.com/urfave/cli/v2"
	"sneak.berlin/go/mfer/internal/checker"
	"sneak.berlin/go/mfer/internal/log"
)

// findManifest looks for a manifest file in the given directory.
// It checks for index.mf and .index.mf, returning the first one found.
func findManifest(fs afero.Fs, dir string) (string, error) {
	candidates := []string{"index.mf", ".index.mf"}
	for _, name := range candidates {
		path := filepath.Join(dir, name)
		exists, err := afero.Exists(fs, path)
		if err != nil {
			return "", err
		}
		if exists {
			return path, nil
		}
	}
	return "", fmt.Errorf("no manifest found in %s (looked for index.mf and .index.mf)", dir)
}

func (mfa *CLIApp) checkManifestOperation(ctx *cli.Context) error {
	log.Debug("checkManifestOperation()")

	var manifestPath string
	var err error

	if ctx.Args().Len() > 0 {
		arg := ctx.Args().Get(0)
		// Check if arg is a directory or a file
		info, statErr := mfa.Fs.Stat(arg)
		if statErr == nil && info.IsDir() {
			// It's a directory, look for manifest inside
			manifestPath, err = findManifest(mfa.Fs, arg)
			if err != nil {
				return err
			}
		} else {
			// Treat as a file path
			manifestPath = arg
		}
	} else {
		// No argument, look in current directory
		manifestPath, err = findManifest(mfa.Fs, ".")
		if err != nil {
			return err
		}
	}

	basePath := ctx.String("base")
	showProgress := ctx.Bool("progress")

	log.Infof("checking manifest %s with base %s", manifestPath, basePath)

	// Create checker
	chk, err := checker.NewChecker(manifestPath, basePath, mfa.Fs)
	if err != nil {
		return fmt.Errorf("failed to load manifest: %w", err)
	}

	log.Infof("manifest contains %d files, %s", chk.FileCount(), humanize.IBytes(uint64(chk.TotalBytes())))

	// Set up results channel
	results := make(chan checker.Result, 1)

	// Set up progress channel
	var progress chan checker.CheckStatus
	if showProgress {
		progress = make(chan checker.CheckStatus, 1)
		go func() {
			for status := range progress {
				if status.ETA > 0 {
					log.Progressf("Checking: %d/%d files, %s/s, ETA %s, %d failures",
						status.CheckedFiles,
						status.TotalFiles,
						humanize.IBytes(uint64(status.BytesPerSec)),
						status.ETA.Round(time.Second),
						status.Failures)
				} else {
					log.Progressf("Checking: %d/%d files, %s/s, %d failures",
						status.CheckedFiles,
						status.TotalFiles,
						humanize.IBytes(uint64(status.BytesPerSec)),
						status.Failures)
				}
			}
			log.ProgressDone()
		}()
	}

	// Process results in a goroutine
	var failures int64
	done := make(chan struct{})
	go func() {
		for result := range results {
			if result.Status != checker.StatusOK {
				failures++
				log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message)
			} else {
				log.Verbosef("%s: %s", result.Status, result.Path)
			}
		}
		close(done)
	}()

	// Run check
	err = chk.Check(ctx.Context, results, progress)
	if err != nil {
		return fmt.Errorf("check failed: %w", err)
	}

	// Wait for results processing to complete
	<-done

	// Check for extra files if requested
	if ctx.Bool("no-extra-files") {
		extraResults := make(chan checker.Result, 1)
		extraDone := make(chan struct{})
		go func() {
			for result := range extraResults {
				failures++
				log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message)
			}
			close(extraDone)
		}()

		err = chk.FindExtraFiles(ctx.Context, extraResults)
		if err != nil {
			return fmt.Errorf("failed to check for extra files: %w", err)
		}
		<-extraDone
	}

	elapsed := time.Since(mfa.startupTime).Seconds()
	rate := float64(chk.TotalBytes()) / elapsed
	if failures == 0 {
		log.Infof("checked %d files (%s) in %.1fs (%s/s): all OK", chk.FileCount(), humanize.IBytes(uint64(chk.TotalBytes())), elapsed, humanize.IBytes(uint64(rate)))
	} else {
		log.Infof("checked %d files (%s) in %.1fs (%s/s): %d failed", chk.FileCount(), humanize.IBytes(uint64(chk.TotalBytes())), elapsed, humanize.IBytes(uint64(rate)), failures)
	}

	if failures > 0 {
		mfa.exitCode = 1
	}

	return nil
}
