// Package engine provides a mode-agnostic scan engine that can be used
// independently of any UI layer. This file defines the main Engine type
// which orchestrates scanning operations.
//
// Concurrency Model:
//
// The Engine is designed for concurrent use. Multiple goroutines can call
// Scan() simultaneously - each scan gets its own context and worker pool.
// The activeScans map tracks all running scans for coordinated shutdown.
//
//	engine := engine.New(cfg, reporter)
//	go engine.Scan(ctx1, targets1)  // Scan 1
//	go engine.Scan(ctx2, targets2)  // Scan 2 (concurrent with 1)
//	incomplete := engine.StopAll()  // Cancel both, get incomplete targets
package engine

import (
	"context"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/google/uuid"
)

// Engine is the core scan engine. It manages HTTP clients, worker pools,
// and coordinates multiple concurrent scans.
//
// Engine is safe for concurrent use. Multiple goroutines can call Scan()
// simultaneously, each running independently with its own context.
//
// Call Close() when done to release resources and cancel any active scans.
type Engine struct {
	config    Config
	reporter  ProgressReporter
	stats     *Stats
	client    *http.Client
	rateLimit *RateLimitState

	// mu protects activeScans map
	mu sync.RWMutex
	// activeScans maps scanID to cancel function for tracking concurrent scans
	activeScans map[string]scanInfo
}

// scanInfo holds information about an active scan for shutdown coordination.
type scanInfo struct {
	cancel  context.CancelFunc
	targets []string
}

// New creates a new Engine with the given configuration and reporter.
// Returns an error if the configuration is invalid.
//
// The reporter receives callbacks for progress, errors, and findings.
// All callbacks may be invoked from multiple goroutines concurrently.
func New(cfg Config, reporter ProgressReporter) (*Engine, error) {
	if err := cfg.Validate(); err != nil {
		return nil, err
	}

	return &Engine{
		config:      cfg,
		reporter:    reporter,
		stats:       &Stats{},
		client:      createHTTPClient(cfg),
		rateLimit:   NewRateLimitState(),
		activeScans: make(map[string]scanInfo),
	}, nil
}

// Scan scans the given targets for credential exposures.
//
// Multiple goroutines can call Scan() concurrently - each scan runs
// independently with its own context and worker pool. Use this to run
// parallel scans or to support multiple users in daemon mode.
//
// The scan respects context cancellation:
//   - Cancelled context stops accepting new paths
//   - In-flight HTTP requests are cancelled (context passed through)
//   - Returns context.Canceled when cancelled
//
// Returns nil on successful completion, context.Canceled if cancelled,
// or the first error from a worker if scanning fails.
func (e *Engine) Scan(ctx context.Context, targets []string) error {
	// Generate unique scan ID
	scanID := uuid.New().String()

	// Create cancellable context
	ctx, cancel := context.WithCancel(ctx)

	// Track this scan
	e.mu.Lock()
	e.activeScans[scanID] = scanInfo{
		cancel:  cancel,
		targets: targets,
	}
	e.mu.Unlock()

	// Cleanup on completion
	defer func() {
		e.mu.Lock()
		delete(e.activeScans, scanID)
		e.mu.Unlock()
		cancel()
	}()

	// Create worker pool for this scan
	pool := newWorkerPool(e.config, e.client, e.reporter, e.stats, e.rateLimit)

	// Run the scan
	return pool.scanTargets(ctx, targets)
}

// StopAll cancels all active scans and waits for them to complete.
// Returns a combined list of targets that were in-flight across all scans.
//
// The shutdown process:
//  1. Cancel all active scan contexts
//  2. Wait up to ShutdownTimeout for scans to complete
//  3. Return list of incomplete targets (for resume use)
//
// After StopAll returns, no scans are running and it's safe to Close().
func (e *Engine) StopAll() []string {
	e.mu.Lock()

	// Collect all targets and cancel all scans
	var incompleteTargets []string
	for _, info := range e.activeScans {
		incompleteTargets = append(incompleteTargets, info.targets...)
		info.cancel()
	}

	scanCount := len(e.activeScans)
	e.mu.Unlock()

	if scanCount == 0 {
		return nil
	}

	// Wait for all scans to complete with timeout
	deadline := time.Now().Add(e.config.ShutdownTimeout)
	for {
		e.mu.RLock()
		remaining := len(e.activeScans)
		e.mu.RUnlock()

		if remaining == 0 {
			break
		}

		if time.Now().After(deadline) {
			// Timeout - some scans didn't finish
			break
		}

		time.Sleep(50 * time.Millisecond)
	}

	return incompleteTargets
}

// Stats returns a snapshot of current engine statistics.
// Safe to call at any time, including during active scans.
func (e *Engine) Stats() StatsSnapshot {
	return e.stats.Snapshot()
}

// ActiveScanCount returns the number of currently running scans.
// Safe to call at any time.
func (e *Engine) ActiveScanCount() int {
	e.mu.RLock()
	defer e.mu.RUnlock()
	return len(e.activeScans)
}

// IsRateLimited returns true if the engine is currently in rate-limited state.
// Use this to display a rate limit indicator in the TUI.
func (e *Engine) IsRateLimited() bool {
	return e.rateLimit.IsLimited()
}

// Close releases all engine resources and cancels any active scans.
// After Close(), the engine should not be used.
func (e *Engine) Close() error {
	// Stop all active scans first
	e.StopAll()

	// Close idle HTTP connections
	e.client.CloseIdleConnections()

	return nil
}

// VulnTypeFromPath extracts vulnerability type from a finding's path.
// Returns short type identifier for stats grouping.
//
// Used by TUI to update per-type stats when OnFinding is called.
func VulnTypeFromPath(path string) string {
	// Map common paths to short identifiers
	switch {
	case strings.HasPrefix(path, "/.git"):
		return ".git"
	case strings.HasPrefix(path, "/.env"):
		return ".env"
	case strings.Contains(path, "wp-config"):
		return "wp-config"
	case strings.Contains(path, "config.php"):
		return "config"
	case strings.Contains(path, ".sql"):
		return "sql-dump"
	case strings.Contains(path, "backup"):
		return "backup"
	case strings.Contains(path, "debug"):
		return "debug"
	case strings.HasPrefix(path, "/.htpasswd"):
		return ".htpasswd"
	case strings.HasPrefix(path, "/.htaccess"):
		return ".htaccess"
	case strings.Contains(path, "phpinfo"):
		return "phpinfo"
	case strings.Contains(path, "credentials"):
		return "credentials"
	case strings.Contains(path, "aws"):
		return "aws"
	case strings.Contains(path, "docker"):
		return "docker"
	default:
		// Use first path segment as type
		parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
		if len(parts) > 0 && len(parts[0]) > 0 {
			return parts[0]
		}
		return "other"
	}
}
