package daemon

import (
	"encoding/json"
	"io"
	"sync"
	"sync/atomic"
	"time"

	"huntr/internal/engine"
)

// Compile-time check: DaemonReporter must satisfy ProgressReporter.
var _ engine.ProgressReporter = (*DaemonReporter)(nil)

// DaemonReporter implements engine.ProgressReporter for headless daemon mode.
// It writes JSONL findings to the provided io.Writer and tracks scan statistics
// for status queries.
//
// All methods are safe for concurrent use from multiple goroutines.
// The mutex protects the JSON encoder from interleaved writes (Pitfall 3).
// Counters use atomic operations for lock-free reads.
type DaemonReporter struct {
	mu      sync.Mutex
	encoder *json.Encoder

	scanned      int64
	found        int64
	errors       int64
	startTime    time.Time
	totalTargets int
}

// DaemonStats holds a snapshot of current scan statistics.
// Returned by Stats() for status queries.
type DaemonStats struct {
	Scanned      int64
	Found        int64
	Errors       int64
	TotalTargets int
	Elapsed      time.Duration
}

// NewDaemonReporter creates a new DaemonReporter that writes JSONL findings
// to the provided writer. The totalTargets parameter is used for progress
// percentage calculation in Stats().
//
// The encoder is configured with SetEscapeHTML(false) to avoid escaping
// &, <, > characters in URLs.
func NewDaemonReporter(w io.Writer, totalTargets int) *DaemonReporter {
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)

	return &DaemonReporter{
		encoder:      enc,
		startTime:    time.Now(),
		totalTargets: totalTargets,
	}
}

// OnProgress is called when a target's scan status changes.
// Increments the scanned counter on completion (StatusCompleted or StatusFound)
// and the errors counter on StatusError.
// No JSONL output is produced (findings only per design decision).
func (d *DaemonReporter) OnProgress(update engine.ProgressUpdate) {
	switch update.Status {
	case engine.StatusCompleted, engine.StatusFound:
		atomic.AddInt64(&d.scanned, 1)
	case engine.StatusError:
		atomic.AddInt64(&d.errors, 1)
	}
}

// OnError is called when an error occurs during scanning.
// Increments the errors counter. No JSONL output is produced
// (findings only per design decision).
func (d *DaemonReporter) OnError(errInfo engine.ErrorInfo) {
	atomic.AddInt64(&d.errors, 1)
}

// OnFinding is called immediately when an exposure is discovered.
// Writes a FindingEvent as one JSON line to the output writer.
// The mutex ensures concurrent calls produce valid JSONL (no interleaving).
func (d *DaemonReporter) OnFinding(finding engine.Finding) {
	atomic.AddInt64(&d.found, 1)

	event := FindingEvent{
		Domain:    finding.Domain,
		Path:      finding.Path,
		Type:      engine.VulnTypeFromPath(finding.Path),
		Timestamp: time.Now().UTC().Format(time.RFC3339),
		Status:    finding.StatusCode,
		Patterns:  finding.Patterns,
	}

	d.mu.Lock()
	d.encoder.Encode(event) //nolint:errcheck // stdout write errors are unrecoverable
	d.mu.Unlock()
}

// Stats returns a snapshot of current scan statistics.
// Safe to call at any time, including during active scans.
// All counters are read atomically for consistency.
func (d *DaemonReporter) Stats() DaemonStats {
	return DaemonStats{
		Scanned:      atomic.LoadInt64(&d.scanned),
		Found:        atomic.LoadInt64(&d.found),
		Errors:       atomic.LoadInt64(&d.errors),
		TotalTargets: d.totalTargets,
		Elapsed:      time.Since(d.startTime),
	}
}
