package npmaudit

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/lukaszraczylo/gohoarder/pkg/config"
	"github.com/lukaszraczylo/gohoarder/pkg/metadata"
	"github.com/lukaszraczylo/gohoarder/pkg/uuid"
	"github.com/rs/zerolog/log"
)

// ScannerName is the name of this scanner
const ScannerName = "npm-audit"

// Scanner implements the npm audit vulnerability scanner
type Scanner struct {
	config config.NpmAuditConfig
}

// New creates a new npm audit scanner
func New(cfg config.NpmAuditConfig) *Scanner {
	return &Scanner{
		config: cfg,
	}
}

// Name returns the scanner name
func (s *Scanner) Name() string {
	return ScannerName
}

// Scan scans an npm package using npm audit
func (s *Scanner) Scan(ctx context.Context, registry, packageName, version string, filePath string) (*metadata.ScanResult, error) {
	// Only scan npm packages
	if registry != "npm" {
		return &metadata.ScanResult{
			ID:                 uuid.New().String(),
			Registry:           registry,
			PackageName:        packageName,
			PackageVersion:     version,
			Scanner:            ScannerName,
			ScannedAt:          time.Now(),
			Status:             metadata.ScanStatusClean,
			VulnerabilityCount: 0,
			Vulnerabilities:    []metadata.Vulnerability{},
			Details: map[string]interface{}{
				"skipped": "npm-audit only supports npm packages",
			},
		}, nil
	}

	log.Info().
		Str("scanner", ScannerName).
		Str("package", packageName).
		Str("version", version).
		Msg("Starting npm audit scan")

	// Create a temporary directory
	tmpDir, err := os.MkdirTemp("", "npm-audit-*")
	if err != nil {
		return nil, fmt.Errorf("failed to create temp dir: %w", err)
	}
	defer os.RemoveAll(tmpDir)

	// Extract the .tgz file
	if err := s.extractTgz(filePath, tmpDir); err != nil {
		return nil, fmt.Errorf("failed to extract tgz: %w", err)
	}

	// Find the package directory (usually "package/")
	packageDir := filepath.Join(tmpDir, "package")
	if _, err := os.Stat(packageDir); os.IsNotExist(err) {
		// Try the tmpDir itself
		packageDir = tmpDir
	}

	// Run npm audit
	cmd := exec.CommandContext(ctx, "npm", "audit", "--json", "--package-lock-only")
	cmd.Dir = packageDir
	output, _ := cmd.CombinedOutput() // npm audit returns non-zero when vulns found

	// Parse npm audit output
	var auditResult NpmAuditResult
	if len(output) > 0 {
		if err := json.Unmarshal(output, &auditResult); err != nil {
			log.Warn().Err(err).Msg("Failed to parse npm audit output")
			// Return clean result on parse error
			return s.emptyResult(registry, packageName, version), nil
		}
	}

	// Convert to our format
	result := s.convertResult(&auditResult, registry, packageName, version)

	log.Info().
		Str("scanner", ScannerName).
		Str("package", packageName).
		Int("vulnerabilities", result.VulnerabilityCount).
		Msg("npm audit scan completed")

	return result, nil
}

// Health checks if npm is available
func (s *Scanner) Health(ctx context.Context) error {
	cmd := exec.CommandContext(ctx, "npm", "--version")
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("npm not available: %w", err)
	}
	return nil
}

// extractTgz extracts a .tgz file
func (s *Scanner) extractTgz(tgzPath, destDir string) error {
	cmd := exec.Command("tar", "-xzf", tgzPath, "-C", destDir)
	return cmd.Run()
}

// emptyResult returns an empty scan result
func (s *Scanner) emptyResult(registry, packageName, version string) *metadata.ScanResult {
	return &metadata.ScanResult{
		ID:                 uuid.New().String(),
		Registry:           registry,
		PackageName:        packageName,
		PackageVersion:     version,
		Scanner:            ScannerName,
		ScannedAt:          time.Now(),
		Status:             metadata.ScanStatusClean,
		VulnerabilityCount: 0,
		Vulnerabilities:    []metadata.Vulnerability{},
		Details:            map[string]interface{}{},
	}
}

// convertResult converts npm audit output to our ScanResult format
func (s *Scanner) convertResult(auditResult *NpmAuditResult, registry, packageName, version string) *metadata.ScanResult {
	vulnerabilities := make([]metadata.Vulnerability, 0)
	severityCounts := make(map[string]int)

	// Process vulnerabilities from the audit result
	for _, vuln := range auditResult.Vulnerabilities {
		// Normalize severity
		normalizedSeverity := metadata.NormalizeSeverity(vuln.Severity)
		severityCounts[normalizedSeverity]++

		// Get references
		refs := make([]string, 0)
		if vuln.URL != "" {
			refs = append(refs, vuln.URL)
		}
		for _, ref := range vuln.References {
			if ref.URL != "" {
				refs = append(refs, ref.URL)
			}
		}

		// Get fixed version
		fixedIn := ""
		if vuln.FixAvailable != nil {
			fixedIn = fmt.Sprintf("%v", vuln.FixAvailable)
		}

		vulnerabilities = append(vulnerabilities, metadata.Vulnerability{
			ID:          vuln.Via,
			Severity:    normalizedSeverity,
			Title:       vuln.Name,
			Description: vuln.Name,
			References:  refs,
			FixedIn:     fixedIn,
		})
	}

	status := metadata.ScanStatusClean
	if len(vulnerabilities) > 0 {
		status = metadata.ScanStatusVulnerable
	}

	return &metadata.ScanResult{
		ID:                 uuid.New().String(),
		Registry:           registry,
		PackageName:        packageName,
		PackageVersion:     version,
		Scanner:            ScannerName,
		ScannedAt:          time.Now(),
		Status:             status,
		VulnerabilityCount: len(vulnerabilities),
		Vulnerabilities:    vulnerabilities,
		Details: map[string]interface{}{
			"severity_counts": severityCounts,
		},
	}
}

// NpmAuditResult represents npm audit JSON output
type NpmAuditResult struct {
	Vulnerabilities    map[string]NpmVulnerability `json:"vulnerabilities"`
	Metadata           NpmAuditMetadata            `json:"metadata"`
	AuditReportVersion int                         `json:"auditReportVersion"`
}

type NpmVulnerability struct {
	Name         string         `json:"name"`
	Severity     string         `json:"severity"`
	Via          string         `json:"via"`
	Effects      []string       `json:"effects"`
	Range        string         `json:"range"`
	FixAvailable interface{}    `json:"fixAvailable"`
	URL          string         `json:"url"`
	References   []NpmReference `json:"references"`
}

type NpmReference struct {
	URL string `json:"url"`
}

type NpmAuditMetadata struct {
	Vulnerabilities NpmVulnCounts `json:"vulnerabilities"`
	Dependencies    int           `json:"dependencies"`
}

type NpmVulnCounts struct {
	Info     int `json:"info"`
	Low      int `json:"low"`
	Moderate int `json:"moderate"`
	High     int `json:"high"`
	Critical int `json:"critical"`
	Total    int `json:"total"`
}
