package vulnscan

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"xorm.io/xorm"

	"github.com/forgeo/forgebucket/internal/events"
	"github.com/forgeo/forgebucket/internal/models"
)

// Scanner watches for SBOM generation events and queries OSV for vulns.
type Scanner struct {
	db     *xorm.Engine
	bus    events.EventBus
	client *Client
}

func NewScanner(db *xorm.Engine, bus events.EventBus) *Scanner {
	return &Scanner{
		db:     db,
		bus:    bus,
		client: NewClient(),
	}
}

// Start subscribes to SBOM-related events and scans for vulnerabilities.
func (s *Scanner) Start(ctx context.Context) {
	// Listen for SBOM Report created events (sync trigger).
	// In practice this is called on-demand via the API, so Start is minimal.
	<-ctx.Done()
}

// ScanByPURL queries OSV for a single package and stores findings.
func (s *Scanner) ScanByPURL(repoID int64, purl, version string) ([]models.VulnerabilityFinding, error) {
	vulns, err := s.client.QueryByPURL(purl, version)
	if err != nil {
		return nil, err
	}
	return s.persistFindings(repoID, purl, version, vulns), nil
}

// ScanSBOM reads the latest SBOM report for a repo, queries OSV for every
// component, and stores the findings.  Returns the new findings.
func (s *Scanner) ScanSBOM(repoID int64) ([]models.VulnerabilityFinding, error) {
	var report models.SBOMReport
	found, err := s.db.Where("repo_id = ?", repoID).
		OrderBy("generated_at DESC").Get(&report)
	if err != nil {
		return nil, err
	}
	if !found {
		return nil, fmt.Errorf("no SBOM found for repo %d", repoID)
	}

	var doc struct {
		Components []struct {
			Name    string `json:"name"`
			Version string `json:"version"`
			PURL    string `json:"purl"`
		} `json:"components"`
	}
	if err := json.Unmarshal([]byte(report.BOMDocument), &doc); err != nil {
		return nil, fmt.Errorf("parse SBOM: %w", err)
	}

	var allFindings []models.VulnerabilityFinding
	for _, comp := range doc.Components {
		if comp.PURL == "" || comp.Version == "" {
			continue
		}
		vulns, err := s.client.QueryByPURL(comp.PURL, comp.Version)
		if err != nil {
			log.Printf("vulnscan: query %s@%s: %v", comp.PURL, comp.Version, err)
			continue
		}
		findings := s.persistFindings(repoID, comp.PURL, comp.Version, vulns)
		allFindings = append(allFindings, findings...)
	}
	return allFindings, nil
}

// ListFindings returns unfixed vulnerability findings for a repo.
func (s *Scanner) ListFindings(repoID int64) ([]models.VulnerabilityFinding, error) {
	var findings []models.VulnerabilityFinding
	if err := s.db.Where("repo_id = ? AND dismissed = ?", repoID, false).
		OrderBy("cvss_score DESC, detected_at DESC").Find(&findings); err != nil {
		return nil, err
	}
	if findings == nil {
		findings = []models.VulnerabilityFinding{}
	}
	return findings, nil
}

// DismissFindings acknowledges a vulnerability finding.
func (s *Scanner) DismissFindings(findingID int64, dismissedBy string) error {
	now := time.Now().UTC()
	affected, err := s.db.ID(findingID).Cols("dismissed", "dismissed_by", "dismissed_at").
		Update(&models.VulnerabilityFinding{
			Dismissed:   true,
			DismissedBy: dismissedBy,
			DismissedAt: &now,
		})
	if err != nil {
		return err
	}
	if affected == 0 {
		return fmt.Errorf("finding %d not found", findingID)
	}
	return nil
}

func (s *Scanner) persistFindings(repoID int64, purl, version string, vulns []OSVVuln) []models.VulnerabilityFinding {var findings []models.VulnerabilityFinding
	for _, v := range vulns {
		// Check for duplicate before inserting.
		existing := &models.VulnerabilityFinding{}
		if has, _ := s.db.Where("vuln_id = ? AND purl = ? AND repo_id = ?", v.ID, purl, repoID).Get(existing); has {
			continue
		}

		cvssScore := parseCVSS(v)

		finding := &models.VulnerabilityFinding{
			RepoID:      repoID,
			VulnID:      v.ID,
			PURL:        purl,
			Version:     version,
			Summary:     truncateStr(v.Summary, 300),
			Details:     v.Details,
			CVSSScore:   cvssScore,
			FixedVersion: extractFixedVersion(v),
			DetectedAt:  time.Now().UTC(),
		}
		if _, err := s.db.Insert(finding); err != nil {
			log.Printf("vulnscan: insert finding %s for %s: %v", v.ID, purl, err)
			continue
		}
		findings = append(findings, *finding)
	}
	return findings
}

// parseCVSS extracts the CVSS score from OSV severity info.
func parseCVSS(v OSVVuln) float64 {
	for _, sev := range v.Severity {
		if sev.Type == "CVSS_V3" || sev.Type == "CVSS_V2" {
			var score float64
			fmt.Sscanf(sev.Score, "%f", &score)
			return score
		}
	}
	return 0
}

// extractFixedVersion tries to extract the fixed version from affected ranges.
func extractFixedVersion(v OSVVuln) string {
	for _, a := range v.Affected {
		for _, r := range a.Ranges {
			for _, e := range r.Events {
				if e.Fixed != "" {
					return e.Fixed
				}
			}
		}
	}
	return ""
}

func truncateStr(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}
