// AASD — API Attack Surface Discovery
//
// Version: 2026-04.1
// Booth-ready application for GITEX 2026.
//
// Workflow:
//   Domain Input → Domain Discovery (domain-scan) → Filter Subdomains →
//   GoTestWAF Scan → DeepSeek AI Narrative → Static HTML Report (UUID) →
//   Email Report (SMTP delivery on request)
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/goccy/go-yaml"
	"github.com/skip2/go-qrcode"

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

// AppConfig holds application-level configuration from config.yaml.
type AppConfig struct {
	Server ServerConfig `yaml:"server"`
	Admin  AdminConfig  `yaml:"admin"`
}

// ServerConfig holds server-level settings.
type ServerConfig struct {
	BaseURL string `yaml:"base_url"`
}

// AdminConfig holds admin dashboard credentials.
type AdminConfig struct {
	Username string `yaml:"username"`
	Password string `yaml:"password"`
}

var (
	projectDir string
)

func main() {
	// Determine project root (where bin/ and reports/ live)
	var err error
	projectDir, err = os.Getwd()
	if err != nil {
		projectDir = "."
	}

	// Read config.yaml for server and admin settings
	var appCfg AppConfig
	if cfgData, err := os.ReadFile(filepath.Join(projectDir, "config.yaml")); err == nil {
		if err := yaml.Unmarshal(cfgData, &appCfg); err != nil {
			fmt.Printf("Warning: failed to parse config.yaml: %v\n", err)
		}
	}

	// Read public base URL: env var takes precedence, fallback to config.yaml
	// Used for QR codes and email report links
	baseURL := os.Getenv("AASD_BASE_URL")
	if baseURL == "" {
		baseURL = appCfg.Server.BaseURL
	}

	// Initialize AI client (reads api_key from config.yaml in working directory)
	deepSeekClient, err := ai.NewClient()
	if err != nil {
		fmt.Printf("Warning: AI client init failed: %v (reports will use fallback)\n", err)
		deepSeekClient = nil
	}
	mailerService := mailer.New(mailer.ConfigFromEnv())
	orch := scanner.NewOrchestrator(projectDir, baseURL, deepSeekClient, mailerService)

	// Create main context for graceful shutdown
	mainCtx, cancel := context.WithCancel(context.Background())
	defer cancel()

	router := gin.Default()
	router.LoadHTMLGlob("templates/*")

	// Serve static files
	router.Static("/static", "./static")
	router.StaticFile("/", "./static/index.html")
	router.Static("/reports", "./reports")

	// QR code generator endpoint
	router.GET("/qrcode", func(c *gin.Context) {
		text := c.Query("text")
		if text == "" {
			text = "AASD-2026"
		}
		png, err := qrcode.Encode(text, qrcode.Medium, 256)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate QR code"})
			return
		}
		c.Data(http.StatusOK, "image/png", png)
	})

	// ----- START: Pipeline Initiation -----
	router.POST("/start", func(c *gin.Context) {
		domain := strings.TrimSpace(c.PostForm("domain"))
		if domain == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Domain is required"})
			return
		}
		if !strings.Contains(domain, ".") {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain format"})
			return
		}
		if strings.ContainsAny(domain, "/\\") {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid domain characters"})
			return
		}
		if len(domain) > 255 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Domain too long"})
			return
		}

		// Start pipeline
		result, err := orch.StartPipeline(mainCtx, domain)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start scan"})
			return
		}

		// Redirect: IPs skip subdomain selection and go straight to scanning
		if netutil.IsIP(domain) {
			c.Redirect(http.StatusFound, "/analysing?token="+result.ReportToken)
		} else {
			c.Redirect(http.StatusFound, "/select-subdomain?token="+result.ReportToken)
		}
	})

	// ----- STATUS: Poll endpoint -----
	router.GET("/scan-status/:token", func(c *gin.Context) {
		token := c.Param("token")

		// Try token-based lookup first
		if result, exists := orch.GetResult(token); exists {
			c.JSON(http.StatusOK, gin.H{
				"domain":   result.Domain,
				"token":    result.ReportToken,
				"status":   result.Status,
				"subdomains_discovered": len(result.Subdomains),
				"subdomains":            result.Subdomains,
				"report":                result.ReportFile,
				"probe_total":           result.ProbeTotal,
				"probe_checked":         result.ProbeChecked,
			})
			return
		}

		// Fallback: domain-based lookup (for backward compat / simulation page)
		// This supports the old polling mechanism where domain is the key
		domain := c.Param("token") // "token" param could also be a domain
		if result := orch.GetResultByDomain(domain); result != nil {
			c.JSON(http.StatusOK, gin.H{
				"domain":   result.Domain,
				"token":    result.ReportToken,
				"status":   result.Status,
				"subdomains_discovered": len(result.Subdomains),
				"subdomains":            result.Subdomains,
				"report":                result.ReportFile,
				"probe_total":           result.ProbeTotal,
				"probe_checked":         result.ProbeChecked,
			})
			return
		}

		c.JSON(http.StatusNotFound, gin.H{"error": "Scan not found"})
	})

	// ----- ADMIN: Dashboard (basic auth) -----
	adminGroup := router.Group("/admin-dashboard", gin.BasicAuth(gin.Accounts{
		appCfg.Admin.Username: appCfg.Admin.Password,
	}))
	adminGroup.GET("", func(c *gin.Context) {
		results := orch.GetAllResults()
		total, completed, running := orch.CountStats()

		// Annotate results with consultant report existence (GoTestWAF output)
		type scanRow struct {
			scanner.ScanResult
			HasConsultantReport bool
		}
		rows := make([]scanRow, len(results))
		for i, r := range results {
			reportPath := filepath.Join(projectDir, "reports", "consultant_"+r.ReportToken+".html")
			_, err := os.Stat(reportPath)
			rows[i] = scanRow{ScanResult: r, HasConsultantReport: err == nil}
		}

		c.HTML(http.StatusOK, "admin.html", gin.H{
			"Results":   rows,
			"Total":     total,
			"Completed": completed,
			"Running":   running,
		})
	})

	// ----- EMAIL: Send report via SMTP -----
	router.POST("/email-report", func(c *gin.Context) {
		var req struct {
			Token string `json:"token"`
			Email string `json:"email"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
			return
		}

		// Validate that an email was provided in the request
		sendTo := strings.TrimSpace(req.Email)
		if sendTo == "" || !strings.Contains(sendTo, "@") || !strings.Contains(sendTo, ".") {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Valid recipient email is required"})
			return
		}

		if err := orch.SendEmailReport(req.Token, sendTo); err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to send email: %v", err)})
			return
		}

		// Create a report summary page for admin
		reportURL := "/reports/visitor_" + req.Token + ".html"

		// Generate QR for the admin to scan (use full URL if baseURL is set)
		qrReportURL := reportURL
		if baseURL != "" {
			qrReportURL = baseURL + reportURL
		}
		qrData := ""
		png, qrErr := qrcode.Encode(qrReportURL, qrcode.Medium, 256)
		if qrErr == nil {
			qrData = "/reports/qr_" + req.Token + ".png"
			os.WriteFile(filepath.Join(projectDir, "reports", "qr_" + req.Token + ".png"), png, 0644)
		}
		_ = qrData

		c.JSON(http.StatusOK, gin.H{
			"message": "Report sent successfully",
			"report":  reportURL,
		})
	})

	// ----- SELECT SUBDOMAIN: User picks a target from discovered subdomains -----
	router.POST("/select-subdomain", func(c *gin.Context) {
		token := strings.TrimSpace(c.PostForm("token"))
		domain := strings.TrimSpace(c.PostForm("domain"))
		if token == "" || domain == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "Token and domain are required"})
			return
		}
		if err := orch.SelectAndScan(mainCtx, token, domain); err != nil {
			// Use 400 for validation errors (early selection), 500 for unexpected failures
			status := http.StatusInternalServerError
			if strings.Contains(err.Error(), "cannot select target") {
				status = http.StatusBadRequest
			}
			c.JSON(status, gin.H{"error": err.Error()})
			return
		}
		c.Redirect(http.StatusFound, "/analysing?token="+token)
	})

	// ----- SELECT SUBDOMAIN: Serve the selection page -----
	router.StaticFile("/select-subdomain", "./static/simulation.html")

	// ----- ANALYSING: Serve the analysis progress page with token support -----
	router.StaticFile("/analysing", "./static/simulation.html")
	// Keep old /simulation route for backward compatibility
	router.StaticFile("/simulation", "./static/simulation.html")

	// ----- DOWNLOAD: Serve the raw scan results for debugging -----
	router.GET("/report-data/:token", func(c *gin.Context) {
		token := c.Param("token")
		result, exists := orch.GetResult(token)
		if !exists {
			c.JSON(http.StatusNotFound, gin.H{"error": "Report not found"})
			return
		}
		c.JSON(http.StatusOK, result)
	})

	// ----- API: List all scan tokens (for admin integration) -----
	router.GET("/api/scans", func(c *gin.Context) {
		results := orch.GetAllResults()
		type scanSummary struct {
			Token  string `json:"token"`
			Domain string `json:"domain"`
			Status string `json:"status"`
			Time   string `json:"created_at"`
		}
		summaries := make([]scanSummary, 0, len(results))
		for _, r := range results {
			summaries = append(summaries, scanSummary{
				Token:  r.ReportToken,
				Domain: r.Domain,
				Status: string(r.Status),
				Time:   r.CreatedAt.Format(time.RFC3339),
			})
		}
		c.JSON(http.StatusOK, summaries)
	})

	// ----- Signal handling + graceful shutdown -----
	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)

	srv := &http.Server{
		Addr:    "0.0.0.0:8080",
		Handler: router,
	}

	go func() {
		fmt.Printf("AASD server starting on %s\n", srv.Addr)
		fmt.Printf("Version: 2026-04.1 | API Attack Surface Discovery\n")
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Printf("Server error: %v\n", err)
		}
	}()

	<-signalChan
	fmt.Println("\nShutdown signal received, shutting down gracefully...")
	cancel()

	shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer shutdownCancel()

	if err := srv.Shutdown(shutdownCtx); err != nil {
		fmt.Printf("Server shutdown error: %v\n", err)
	}
	fmt.Println("AASD server stopped")
}
