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

package security

import (
	"context"

	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
	"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)

func init() {
	db.RegisterModel(new(SecurityAlert))
	db.RegisterModel(new(SecurityScannerConfig))
}

// AlertSeverity represents the severity level of a security finding.
type AlertSeverity string

const (
	SeverityCritical AlertSeverity = "critical"
	SeverityHigh     AlertSeverity = "high"
	SeverityMedium   AlertSeverity = "medium"
	SeverityLow      AlertSeverity = "low"
	SeverityInfo     AlertSeverity = "info"
)

// AlertStatus represents the lifecycle state of an alert.
type AlertStatus string

const (
	AlertStatusActive    AlertStatus = "active"
	AlertStatusResolved  AlertStatus = "resolved"
	AlertStatusDismissed AlertStatus = "dismissed"
)

// ScannerType identifies which scanner produced a finding.
type ScannerType string

const (
	ScannerSecret     ScannerType = "secret"
	ScannerDependency ScannerType = "dependency"
	ScannerCode       ScannerType = "code"
	ScannerConfig     ScannerType = "config"
	ScannerLicense    ScannerType = "license"
)

// SecurityAlert stores a single security finding for a repository.
type SecurityAlert struct {
	ID          int64              `xorm:"pk autoincr"`
	RepoID      int64              `xorm:"INDEX NOT NULL 'repo_id'"`
	Scanner     ScannerType        `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
	Severity    AlertSeverity      `xorm:"VARCHAR(10) NOT NULL 'severity'"`
	Status      AlertStatus        `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
	RuleID      string             `xorm:"VARCHAR(100) NOT NULL 'rule_id'"`   // e.g. "aws-access-key", "cve-2024-1234"
	Title       string             `xorm:"TEXT NOT NULL 'title'"`
	Description string             `xorm:"TEXT 'description'"`
	FilePath    string             `xorm:"TEXT 'file_path'"`
	LineNumber  int                `xorm:"'line_number'"`
	CommitSHA   string             `xorm:"VARCHAR(64) 'commit_sha'"`
	Fingerprint string             `xorm:"VARCHAR(64) INDEX 'fingerprint'"` // dedup key: hash of rule+file+content
	Metadata    string             `xorm:"TEXT 'metadata'"`                  // JSON extra data
	ResolvedBy  int64              `xorm:"'resolved_by'"`
	CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
	UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}

func (SecurityAlert) TableName() string {
	return "security_alert"
}

// SecurityScannerConfig stores per-repo scanner settings.
type SecurityScannerConfig struct {
	ID              int64              `xorm:"pk autoincr"`
	RepoID          int64              `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
	Enabled         bool               `xorm:"NOT NULL DEFAULT true 'enabled'"`
	BlockOnPush     bool               `xorm:"NOT NULL DEFAULT false 'block_on_push'"`     // reject push if secrets found
	SecretScanner   bool               `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
	DependScanner   bool               `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
	CodeScanner     bool               `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
	ConfigScanner   bool               `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
	LicenseScanner  bool               `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
	CustomPatterns  string             `xorm:"TEXT 'custom_patterns'"`                      // JSON array of custom regex patterns
	CreatedUnix     timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
	UpdatedUnix     timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}

func (SecurityScannerConfig) TableName() string {
	return "security_scanner_config"
}

// ──────────────────────────────────────────────────────────────────────
// Alert queries
// ──────────────────────────────────────────────────────────────────────

// GetActiveAlerts returns all active alerts for a repo.
func GetActiveAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
	alerts := make([]*SecurityAlert, 0, 20)
	return alerts, db.GetEngine(ctx).
		Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
		OrderBy("severity ASC, created_unix DESC").
		Find(&alerts)
}

// GetAllAlerts returns all alerts for a repo (including resolved/dismissed).
func GetAllAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
	alerts := make([]*SecurityAlert, 0, 50)
	return alerts, db.GetEngine(ctx).
		Where("repo_id = ?", repoID).
		OrderBy("status ASC, severity ASC, created_unix DESC").
		Find(&alerts)
}

// GetAlertByID returns a single alert.
func GetAlertByID(ctx context.Context, id int64) (*SecurityAlert, error) {
	alert := new(SecurityAlert)
	has, err := db.GetEngine(ctx).ID(id).Get(alert)
	if err != nil {
		return nil, err
	}
	if !has {
		return nil, db.ErrNotExist{Resource: "SecurityAlert", ID: id}
	}
	return alert, nil
}

// GetAlertCountsByRepo returns count of active alerts grouped by severity.
func GetAlertCountsByRepo(ctx context.Context, repoID int64) (map[AlertSeverity]int64, error) {
	type result struct {
		Severity AlertSeverity `xorm:"severity"`
		Count    int64         `xorm:"count"`
	}
	var results []result
	err := db.GetEngine(ctx).
		Table("security_alert").
		Select("severity, COUNT(*) as count").
		Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
		GroupBy("severity").
		Find(&results)
	if err != nil {
		return nil, err
	}
	counts := make(map[AlertSeverity]int64)
	for _, r := range results {
		counts[r.Severity] = r.Count
	}
	return counts, nil
}

// CreateOrUpdateAlert creates a new alert or updates if fingerprint exists.
func CreateOrUpdateAlert(ctx context.Context, alert *SecurityAlert) error {
	if alert.Fingerprint != "" {
		existing := new(SecurityAlert)
		has, err := db.GetEngine(ctx).
			Where("repo_id = ? AND fingerprint = ?", alert.RepoID, alert.Fingerprint).
			Get(existing)
		if err != nil {
			return err
		}
		if has {
			// Update existing - refresh commit SHA and keep active
			existing.CommitSHA = alert.CommitSHA
			existing.LineNumber = alert.LineNumber
			existing.Status = AlertStatusActive
			_, err = db.GetEngine(ctx).ID(existing.ID).
				Cols("commit_sha", "line_number", "status").Update(existing)
			return err
		}
	}
	_, err := db.GetEngine(ctx).Insert(alert)
	return err
}

// UpdateAlertStatus changes the status of an alert.
func UpdateAlertStatus(ctx context.Context, id int64, status AlertStatus, resolvedBy int64) error {
	_, err := db.GetEngine(ctx).ID(id).
		Cols("status", "resolved_by").
		Update(&SecurityAlert{Status: status, ResolvedBy: resolvedBy})
	return err
}

// ──────────────────────────────────────────────────────────────────────
// Scanner config queries
// ──────────────────────────────────────────────────────────────────────

// GetScannerConfig returns the scanner config for a repo, or defaults.
func GetScannerConfig(ctx context.Context, repoID int64) (*SecurityScannerConfig, error) {
	cfg := new(SecurityScannerConfig)
	has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
	if err != nil {
		return nil, err
	}
	if !has {
		return &SecurityScannerConfig{
			RepoID:        repoID,
			Enabled:       true,
			SecretScanner: true,
			DependScanner: true,
		}, nil
	}
	return cfg, nil
}

// SaveScannerConfig creates or updates scanner config.
func SaveScannerConfig(ctx context.Context, cfg *SecurityScannerConfig) error {
	existing := new(SecurityScannerConfig)
	has, err := db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
	if err != nil {
		return err
	}
	if has {
		cfg.ID = existing.ID
		_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
		return err
	}
	_, err = db.GetEngine(ctx).Insert(cfg)
	return err
}
