// Package scanner orchestrates the full API Attack Surface Discovery pipeline:
// Domain Input → Domain Discovery → Filter Subdomains → GoTestWAF Scan → AI Report.
package scanner

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"aasd/internal/ai"
	"aasd/internal/mailer"
	"aasd/internal/report"
	"aasd/pkg/netutil"
)

// ScanStatus enumerates the pipeline phases.
type ScanStatus string

const (
	StatusPending             ScanStatus = "pending"
	StatusDiscovering         ScanStatus = "discovering"
	StatusAwaitingSelection   ScanStatus = "awaiting_selection"
	StatusScanning            ScanStatus = "scanning"
	StatusGenerating          ScanStatus = "generating"
	StatusCompleted           ScanStatus = "completed"
	StatusFailed              ScanStatus = "failed"
)

// ScanResult holds the full state of a scan pipeline execution.
type ScanResult struct {
	Domain         string     `json:"domain"`
	Subdomains     []string   `json:"subdomains,omitempty"`
	SelectedDomain string     `json:"selected_domain,omitempty"`
	Status         ScanStatus `json:"status"`
	ReportToken    string     `json:"token"`
	ReportFile     string     `json:"report_file,omitempty"`
	AIReportFile   string     `json:"ai_report_file,omitempty"`
	Error          string     `json:"error,omitempty"`
	ProbeTotal     int        `json:"probe_total"`
	ProbeChecked   int        `json:"probe_checked"`
	CreatedAt      time.Time  `json:"created_at"`
	CompletedAt    time.Time  `json:"completed_at,omitempty"`
}

// Orchestrator manages the scan pipeline execution.
type Orchestrator struct {
	mu          sync.RWMutex
	results     map[string]*ScanResult // keyed by report token
	projectRoot string
	baseURL     string // public base URL for report links (e.g. https://coding.sechpoint.app)
	aiClient    *ai.Client
	mailer      *mailer.Mailer
}

// NewOrchestrator creates a new scan pipeline orchestrator.
// baseURL is the public-facing base URL used in email report links.
func NewOrchestrator(projectRoot, baseURL string, aiClient *ai.Client, m *mailer.Mailer) *Orchestrator {
	return &Orchestrator{
		results:     make(map[string]*ScanResult),
		projectRoot: projectRoot,
		baseURL:     baseURL,
		aiClient:    aiClient,
		mailer:      m,
	}
}

// StartPipeline kicks off the discovery phase in a goroutine.
// Returns the ScanResult immediately with status "pending".
// The pipeline pauses at "awaiting_selection" — the frontend displays
// discovered subdomains and the user picks which one to scan.
func (o *Orchestrator) StartPipeline(ctx context.Context, domain string) (*ScanResult, error) {
	token, err := report.GenerateToken()
	if err != nil {
		return nil, fmt.Errorf("scanner: failed to generate token: %w", err)
	}

	result := &ScanResult{
		Domain:      domain,
		Subdomains:  []string{},
		Status:      StatusPending,
		ReportToken: token,
		CreatedAt:   time.Now(),
	}

	o.mu.Lock()
	o.results[token] = result
	o.mu.Unlock()

	// Phase 1: Domain Discovery or direct scan
	if netutil.IsIP(result.Domain) {
		// IP address — no subdomains to discover, scan directly.
		// Set Subdomains to [IP] so GenerateFallbackHTML doesn't panic on empty slice.
		result.Subdomains = []string{result.Domain}
		result.SelectedDomain = result.Domain
		go o.executeScanPhase(ctx, result)
	} else {
		// Domain name — run passive discovery + DNS probe
		go o.discoverSubdomains(ctx, result)
	}

	return result, nil
}

// SelectAndScan stores the user's chosen subdomain and kicks off
// the scan phase in a background goroutine.
// Returns an error if discovery hasn't completed yet.
func (o *Orchestrator) SelectAndScan(ctx context.Context, token, selectedDomain string) error {
	o.mu.Lock()
	result, exists := o.results[token]
	o.mu.Unlock()
	if !exists {
		return fmt.Errorf("scanner: no scan found for token %s", token)
	}

	if result.Status != StatusAwaitingSelection {
		return fmt.Errorf("scanner: cannot select target: current status is %q (waiting for %q)",
			result.Status, StatusAwaitingSelection)
	}

	result.SelectedDomain = selectedDomain

	// Ensure Subdomains has at least the selected domain (defensive —
	// prevents panic in report generation when discovery returned nothing)
	if len(result.Subdomains) == 0 {
		result.Subdomains = []string{selectedDomain}
	}

	go o.executeScanPhase(ctx, result)
	return nil
}

// discoverSubdomains runs the subdomain discovery phase, then pauses
// for the user to pick a target from the frontend.
// Uses the wordlist-based HTTPS/TLS probe directly — domain-scan
// (subfinder) was previously used but requires API keys that are not
// available in the booth environment.
func (o *Orchestrator) discoverSubdomains(ctx context.Context, result *ScanResult) {
	o.updateStatus(result.ReportToken, StatusDiscovering)

	subdomains := ProbeSubdomains(ctx, o.projectRoot, result.Domain,
		func(checked, total int) {
			result.ProbeTotal = total
			result.ProbeChecked = checked
		})
	if len(subdomains) == 0 {
		subdomains = []string{result.Domain}
	}
	result.Subdomains = subdomains

	// Only pause for selection if the scan phase hasn't been triggered already.
	// Prevents race where SelectAndScan is called before discovery finishes.
	o.mu.Lock()
	if result.Status == StatusDiscovering {
		result.Status = StatusAwaitingSelection
	}
	o.mu.Unlock()
}

// executeScanPhase runs the GoTestWAF scan, AI narrative, and report generation
// for the user-selected subdomain.
func (o *Orchestrator) executeScanPhase(ctx context.Context, result *ScanResult) {
	selected := result.SelectedDomain

	// Phase 2: GoTestWAF Scan against the selected subdomain
	o.updateStatus(result.ReportToken, StatusScanning)
	scanReportName := fmt.Sprintf("consultant_%s", result.ReportToken)
	_, scanErr := RunGoTestWAF(ctx, o.projectRoot, scanReportName, selected)

	// Read the consultant report for AI (if scan succeeded)
	var consultantHTML []byte
	if scanErr == nil {
		gtwPath := filepath.Join(o.projectRoot, "reports", scanReportName+".html")
		consultantHTML, _ = os.ReadFile(gtwPath)
	} else {
		result.Error = fmt.Sprintf("scan error: %v", scanErr)
	}

	// Phase 3: AI Narrative Generation
	o.updateStatus(result.ReportToken, StatusGenerating)
	var aiHTML string
	var aiErr error
	if o.aiClient != nil && len(consultantHTML) > 0 {
		aiHTML, aiErr = o.aiClient.GenerateResilienceReport(string(consultantHTML))
	}
	if o.aiClient == nil || len(consultantHTML) == 0 || aiErr != nil {
		aiHTML = report.GenerateFallbackHTML(result.Subdomains)
	}

	// Phase 4: Static HTML Report
	reportGen := report.New(o.projectRoot+"/reports", o.baseURL)
	reportPath, err := reportGen.BuildReport(result.ReportToken, result.Domain, aiHTML)
	if err != nil {
		result.Error = fmt.Sprintf("report generation error: %v", err)
		o.updateStatus(result.ReportToken, StatusFailed)
		return
	}

	result.AIReportFile = reportPath
	result.ReportFile = "/reports/visitor_" + result.ReportToken + ".html"

	// Inject email-send UI into the GoTestWAF consultant report
	if scanErr == nil {
		gtwPath := filepath.Join(o.projectRoot, "reports", scanReportName+".html")
		if data, err := os.ReadFile(gtwPath); err == nil {
			emailBlock := fmt.Sprintf(`
    <!-- Email Report Form -->
    <div style="text-align:center; margin-top:32px; padding:24px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0; max-width:480px; margin-left:auto; margin-right:auto;">
      <h2 style="font-size:18px; font-weight:700; color:#1e293b; margin-bottom:4px;">Send this report</h2>
      <p style="font-size:13px; color:#64748b; margin-bottom:16px;">Enter your email and we'll send you the full report</p>
      <input type="email" id="emailInput" placeholder="your@email.com" style="width:100%%; padding:12px 16px; border:2px solid #cbd5e1; border-radius:8px; font-size:15px; box-sizing:border-box; margin-bottom:12px; outline:none;" onfocus="this.style.borderColor='#3b82f6'" onblur="this.style.borderColor='#cbd5e1'">
      <button onclick="sendEmail()" style="width:100%%; padding:12px; background:linear-gradient(90deg, #3b82f6, #8b5cf6); color:white; border:none; border-radius:8px; font-size:15px; font-weight:600; cursor:pointer;">Send Report</button>
      <div id="emailStatus" style="margin-top:10px; font-size:13px; color:#64748b;"></div>
    </div>
    <script>
    function sendEmail() {
      var btn = event.target;
      var email = document.getElementById('emailInput').value.trim();
      var status = document.getElementById('emailStatus');
      if (!email || !email.includes('@')) {
        status.style.color = '#dc2626';
        status.textContent = 'Please enter a valid email address.';
        return;
      }
      btn.disabled = true;
      btn.textContent = 'Sending...';
      status.style.color = '#64748b';
      status.textContent = 'Sending...';
      fetch('/email-report', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({token: '%s', email: email})
      })
      .then(function(r) { return r.json(); })
      .then(function(d) {
        if (d.error) {
          status.style.color = '#dc2626';
          status.textContent = 'Error: ' + d.error;
        } else {
          status.style.color = '#16a34a';
          status.textContent = 'Report sent! Check your inbox.';
          document.getElementById('emailInput').value = '';
        }
      })
      .catch(function() {
        status.style.color = '#dc2626';
        status.textContent = 'Network error. Please try again.';
      })
      .finally(function() {
        btn.disabled = false;
        btn.textContent = 'Send Report';
      });
    }
    </script>`, result.ReportToken)
			modified := strings.Replace(string(data), "</main>", emailBlock+"\n    </main>", 1)
			if err := os.WriteFile(gtwPath, []byte(modified), 0644); err != nil {
				fmt.Printf("scanner: failed to inject email form into GoTestWAF report: %v\n", err)
			}
		}
	}

	result.CompletedAt = time.Now()
	o.updateStatus(result.ReportToken, StatusCompleted)
}

// updateStatus atomically updates the status of a scan result.
func (o *Orchestrator) updateStatus(token string, status ScanStatus) {
	o.mu.Lock()
	defer o.mu.Unlock()
	if r, exists := o.results[token]; exists {
		r.Status = status
	}
}

// GetResult returns the scan result for a given token.
func (o *Orchestrator) GetResult(token string) (*ScanResult, bool) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	r, exists := o.results[token]
	if !exists {
		return nil, false
	}
	cp := *r
	return &cp, true
}

// GetResultByDomain returns the scan result for a given domain (for polling).
func (o *Orchestrator) GetResultByDomain(domain string) *ScanResult {
	o.mu.RLock()
	defer o.mu.RUnlock()
	for _, r := range o.results {
		if r.Domain == domain {
			cp := *r
			return &cp
		}
	}
	return nil
}

// GetAllResults returns all scan results (for admin dashboard).
func (o *Orchestrator) GetAllResults() []ScanResult {
	o.mu.RLock()
	defer o.mu.RUnlock()
	results := make([]ScanResult, 0, len(o.results))
	for _, r := range o.results {
		results = append(results, *r)
	}
	return results
}

// CountStats returns summary statistics for the dashboard.
func (o *Orchestrator) CountStats() (total, completed, running int) {
	o.mu.RLock()
	defer o.mu.RUnlock()
	for _, r := range o.results {
		total++
		switch r.Status {
		case StatusCompleted:
			completed++
		case StatusDiscovering, StatusAwaitingSelection, StatusScanning, StatusGenerating:
			running++
		}
	}
	return
}

// SendEmailReport sends the report link via SMTP.
func (o *Orchestrator) SendEmailReport(token, toEmail string) error {
	result, exists := o.GetResult(token)
	if !exists {
		return fmt.Errorf("report not found for token: %s", token)
	}
	if result.Status != StatusCompleted {
		return fmt.Errorf("report not ready (status: %s)", result.Status)
	}

	reportURL := "/reports/visitor_" + token + ".html"
	if o.baseURL != "" {
		reportURL = o.baseURL + reportURL
	}
	return o.mailer.SendReport(toEmail, reportURL)
}
