// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later

package security

import (
	"bufio"
	"crypto/sha256"
	"fmt"
	"io"
	"regexp"
	"strings"

	security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)

// SecretRule defines a pattern to match against file contents.
type SecretRule struct {
	ID          string
	Title       string
	Pattern     *regexp.Regexp
	Severity    security_model.AlertSeverity
	Description string
}

// DefaultSecretRules contains the built-in secret detection patterns.
var DefaultSecretRules = []SecretRule{
	// AWS
	{ID: "aws-access-key", Title: "AWS Access Key ID", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`), Description: "AWS access key ID detected"},
	{ID: "aws-secret-key", Title: "AWS Secret Access Key", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`(?i)aws_secret_access_key\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}`), Description: "AWS secret access key detected"},

	// Generic tokens/keys
	{ID: "private-key", Title: "Private Key", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----`), Description: "Private key file detected"},
	{ID: "generic-api-key", Title: "Generic API Key", Severity: security_model.SeverityHigh,
		Pattern: regexp.MustCompile(`(?i)(api[_-]?key|apikey)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}`), Description: "API key assignment detected"},
	{ID: "generic-secret", Title: "Generic Secret", Severity: security_model.SeverityHigh,
		Pattern: regexp.MustCompile(`(?i)(secret|password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]`), Description: "Hardcoded secret or password detected"},
	{ID: "generic-token", Title: "Generic Token", Severity: security_model.SeverityHigh,
		Pattern: regexp.MustCompile(`(?i)(token|auth_token|access_token)\s*[=:]\s*['"]?[A-Za-z0-9_\-.]{20,}`), Description: "Token assignment detected"},

	// GitHub/Gitea
	{ID: "github-pat", Title: "GitHub Personal Access Token", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`ghp_[A-Za-z0-9]{36}`), Description: "GitHub personal access token detected"},
	{ID: "github-oauth", Title: "GitHub OAuth Token", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`gho_[A-Za-z0-9]{36}`), Description: "GitHub OAuth token detected"},

	// Stripe
	{ID: "stripe-secret", Title: "Stripe Secret Key", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`sk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live secret key detected"},
	{ID: "stripe-publishable", Title: "Stripe Publishable Key", Severity: security_model.SeverityLow,
		Pattern: regexp.MustCompile(`pk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live publishable key detected (usually safe but flagged)"},

	// JWT
	{ID: "jwt-token", Title: "JWT Token", Severity: security_model.SeverityMedium,
		Pattern: regexp.MustCompile(`eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}`), Description: "JWT token detected"},

	// Connection strings
	{ID: "connection-string", Title: "Connection String with Password", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`(?i)(mysql|postgres|postgresql|mongodb|redis|amqp|smtp)://[^:]+:[^@]+@[^\s]+`), Description: "Database/service connection string with embedded password"},

	// Google
	{ID: "google-api-key", Title: "Google API Key", Severity: security_model.SeverityHigh,
		Pattern: regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`), Description: "Google API key detected"},

	// Slack
	{ID: "slack-webhook", Title: "Slack Webhook URL", Severity: security_model.SeverityMedium,
		Pattern: regexp.MustCompile(`https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+`), Description: "Slack webhook URL detected"},

	// SendGrid
	{ID: "sendgrid-api-key", Title: "SendGrid API Key", Severity: security_model.SeverityHigh,
		Pattern: regexp.MustCompile(`SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}`), Description: "SendGrid API key detected"},

	// PayPal
	{ID: "paypal-client-secret", Title: "PayPal Client Secret", Severity: security_model.SeverityCritical,
		Pattern: regexp.MustCompile(`(?i)paypal.*secret\s*[=:]\s*['"]?[A-Za-z0-9_-]{20,}`), Description: "PayPal client secret detected"},
}

// Files to skip during scanning.
var skipExtensions = map[string]bool{
	".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".ico": true,
	".svg": true, ".woff": true, ".woff2": true, ".ttf": true, ".eot": true,
	".zip": true, ".tar": true, ".gz": true, ".bz2": true, ".7z": true,
	".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true,
	".exe": true, ".dll": true, ".so": true, ".dylib": true, ".o": true,
	".min.js": true, ".min.css": true,
}

var skipPaths = []string{
	"vendor/", "node_modules/", ".git/", "dist/", "build/",
	"go.sum", "package-lock.json", "composer.lock", "yarn.lock",
}

// SecretScanner implements the Scanner interface for secret detection.
type SecretScanner struct {
	Rules []SecretRule
}

// NewSecretScanner creates a scanner with default rules.
func NewSecretScanner() *SecretScanner {
	return &SecretScanner{Rules: DefaultSecretRules}
}

func (s *SecretScanner) Type() security_model.ScannerType {
	return security_model.ScannerSecret
}

func (s *SecretScanner) ScanCommit(commit *git.Commit) ([]Finding, error) {
	// For push-time scanning, we scan the diff of the commit
	return s.ScanTree(commit)
}

func (s *SecretScanner) ScanTree(commit *git.Commit) ([]Finding, error) {
	if commit == nil {
		return nil, nil
	}

	entries, err := commit.ListEntriesRecursiveFast()
	if err != nil {
		return nil, fmt.Errorf("ListEntriesRecursiveFast: %w", err)
	}

	var findings []Finding
	for _, entry := range entries {
		if !entry.IsRegular() {
			continue
		}

		path := entry.Name()
		if shouldSkipFile(path) {
			continue
		}

		// Skip large files (> 1MB)
		if entry.Blob().Size() > 1024*1024 {
			continue
		}

		reader, err := entry.Blob().DataAsync()
		if err != nil {
			log.Trace("SecretScanner: skip %s: %v", path, err)
			continue
		}

		fileFindings := s.scanReader(reader, path, commit.ID.String())
		reader.Close()
		findings = append(findings, fileFindings...)
	}

	return findings, nil
}

func (s *SecretScanner) scanReader(r io.Reader, filePath, commitSHA string) []Finding {
	var findings []Finding
	scanner := bufio.NewScanner(r)
	lineNum := 0

	for scanner.Scan() {
		lineNum++
		line := scanner.Text()

		for _, rule := range s.Rules {
			if rule.Pattern.MatchString(line) {
				fingerprint := fmt.Sprintf("%x", sha256.Sum256([]byte(rule.ID+":"+filePath+":"+line)))
				findings = append(findings, Finding{
					Scanner:     security_model.ScannerSecret,
					Severity:    rule.Severity,
					RuleID:      rule.ID,
					Title:       rule.Title,
					Description: rule.Description,
					FilePath:    filePath,
					LineNumber:  lineNum,
					CommitSHA:   commitSHA,
					Fingerprint: fingerprint[:32],
				})
				break // one finding per line per file
			}
		}
	}
	return findings
}

func shouldSkipFile(path string) bool {
	lower := strings.ToLower(path)

	for _, skip := range skipPaths {
		if strings.HasPrefix(lower, skip) || strings.Contains(lower, "/"+skip) {
			return true
		}
	}

	for ext := range skipExtensions {
		if strings.HasSuffix(lower, ext) {
			return true
		}
	}

	return false
}
