package repository

import (
	"context"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"git.jabali-panel.com/shukivaknin/jabali2/panel-api/internal/models"
)

// MalwareUserScanRepository owns the per-username last-scan tracking
// row (one row per Linux user). Upserted at scan start, finalised by
// the post-scan-hook ingest. The Manual Scan UI lists ALL users left-
// joined against this table, so a user with no row simply renders blank
// "Last scanned" + status="—".
type MalwareUserScanRepository interface {
	StartScan(ctx context.Context, username, sessionID, targetPath string, userID *string) error
	FinishScan(ctx context.Context, sessionID string, state string, hits uint, exitCode *int) error
	FinishScanByPath(ctx context.Context, targetPath string, state string, hits uint, exitCode *int) error
	GetByUsername(ctx context.Context, username string) (*models.MalwareUserScan, error)
	List(ctx context.Context) ([]models.MalwareUserScan, error)
}

type malwareUserScanRepo struct{ db *gorm.DB }

// NewMalwareUserScanRepository returns a GORM-backed repo.
func NewMalwareUserScanRepository(db *gorm.DB) MalwareUserScanRepository {
	return &malwareUserScanRepo{db: db}
}

// StartScan upserts the row with state=running, finished_at=NULL.
// Overwrites any prior finished row for the same username — only the
// most recent scan is tracked (history lives in malware_events).
func (r *malwareUserScanRepo) StartScan(ctx context.Context, username, sessionID, targetPath string, userID *string) error {
	row := models.MalwareUserScan{
		Username:   username,
		UserID:     userID,
		SessionID:  sessionID,
		TargetPath: targetPath,
		State:      models.MalwareUserScanStateRunning,
		Hits:       0,
		ExitCode:   nil,
		StartedAt:  time.Now().UTC(),
		FinishedAt: nil,
	}
	err := r.db.WithContext(ctx).
		Clauses(clause.OnConflict{
			Columns: []clause.Column{{Name: "username"}},
			DoUpdates: clause.AssignmentColumns([]string{
				"user_id", "session_id", "target_path", "state",
				"hits", "exit_code", "started_at", "finished_at",
			}),
		}).
		Create(&row).Error
	if err != nil {
		return translate(err)
	}
	return nil
}

// FinishScan marks the row matching session_id as finished.
func (r *malwareUserScanRepo) FinishScan(ctx context.Context, sessionID string, state string, hits uint, exitCode *int) error {
	now := time.Now().UTC()
	updates := map[string]any{
		"state":       state,
		"hits":        hits,
		"finished_at": now,
	}
	if exitCode != nil {
		updates["exit_code"] = *exitCode
	}
	res := r.db.WithContext(ctx).
		Model(&models.MalwareUserScan{}).
		Where("session_id = ? AND state = ?", sessionID, models.MalwareUserScanStateRunning).
		Updates(updates)
	if res.Error != nil {
		return translate(res.Error)
	}
	return nil
}

// FinishScanByPath is the fallback when the post-scan hook payload does
// not carry our session_id (LMD generates its own scan-id). Match by
// target_path + state=running. There is a small race if the same path
// gets re-scanned mid-flight, but the UI prevents double-start.
func (r *malwareUserScanRepo) FinishScanByPath(ctx context.Context, targetPath string, state string, hits uint, exitCode *int) error {
	now := time.Now().UTC()
	updates := map[string]any{
		"state":       state,
		"hits":        hits,
		"finished_at": now,
	}
	if exitCode != nil {
		updates["exit_code"] = *exitCode
	}
	res := r.db.WithContext(ctx).
		Model(&models.MalwareUserScan{}).
		Where("target_path = ? AND state = ?", targetPath, models.MalwareUserScanStateRunning).
		Updates(updates)
	if res.Error != nil {
		return translate(res.Error)
	}
	return nil
}

func (r *malwareUserScanRepo) GetByUsername(ctx context.Context, username string) (*models.MalwareUserScan, error) {
	var row models.MalwareUserScan
	if err := r.db.WithContext(ctx).Where("username = ?", username).First(&row).Error; err != nil {
		return nil, translate(err)
	}
	return &row, nil
}

func (r *malwareUserScanRepo) List(ctx context.Context) ([]models.MalwareUserScan, error) {
	var rows []models.MalwareUserScan
	if err := r.db.WithContext(ctx).Order("started_at DESC").Find(&rows).Error; err != nil {
		return nil, translate(err)
	}
	return rows, nil
}
