// Malware + YARA incident-response CLI (Gitea #552). Mirrors the Security
// malware API (security_malware.go): scan, scan-status, events, quarantine
// list/restore/delete, signature update, YARA management, and settings. Agent
// verbs stay authoritative; quarantine restore/delete and YARA upload/toggle
// also update the same DB rows the handlers do. Mutations are CLI-audited.
package main

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"text/tabwriter"
	"time"

	"github.com/spf13/cobra"

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

func newMalwareCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "malware",
		Short: "Malware scan / quarantine / YARA / settings (incident response)",
	}
	cmd.AddCommand(
		csPassthroughCmd("status", "Show malware engine status", "security.malware.status"),
		newMalwareScanCmd(),
		newMalwareScanStatusCmd(),
		newMalwareEventsCmd(),
		newMalwareQuarantineCmd(),
		csPassthroughCmd("update-signatures", "Update malware signatures (freshclam/maldet)", "security.malware.update_signatures"),
		newMalwareYaraCmd(),
		newMalwareSettingsCmd(),
	)
	return cmd
}

func newMalwareScanCmd() *cobra.Command {
	var path, user string
	cmd := &cobra.Command{
		Use:     "scan",
		Short:   "Start a malware scan of a path or a user's home",
		Args:    cobra.NoArgs,
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, _ []string) error {
			if (path == "") == (user == "") {
				return fmt.Errorf("exactly one of --path or --user is required")
			}
			verb, params := "security.malware.scan_path", map[string]any{"path": path}
			if user != "" {
				verb, params = "security.malware.scan_user", map[string]any{"user": user}
			}
			ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
			defer cancel()
			err := csCall(ctx, verb, params)
			if err == nil {
				cliAuditOK(ctx, "malware.scan", "malware_scan", path+user, nil)
			}
			return err
		},
	}
	cmd.Flags().StringVar(&path, "path", "", "absolute path to scan")
	cmd.Flags().StringVar(&user, "user", "", "username whose home to scan")
	return cmd
}

func newMalwareScanStatusCmd() *cobra.Command {
	return &cobra.Command{
		Use:     "scan-status <session-id>",
		Short:   "Poll a scan session's status",
		Args:    cobra.ExactArgs(1),
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, args []string) error {
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			return csCall(ctx, "security.malware.scan_status", map[string]any{"session_id": args[0]})
		},
	}
}

func newMalwareEventsCmd() *cobra.Command {
	var source, severity, user string
	cmd := &cobra.Command{
		Use:     "events",
		Short:   "List malware detection events",
		Args:    cobra.NoArgs,
		PreRunE: requireDB,
		RunE: func(cmd *cobra.Command, _ []string) error {
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			rows, _, err := repository.NewMalwareEventRepository(sharedDB).List(ctx, repository.MalwareEventListFilter{
				Source: source, Severity: severity, UserID: user,
			})
			if err != nil {
				return err
			}
			if jsonOutput {
				return printJSON(rows)
			}
			tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
			fmt.Fprintln(tw, "ID\tSOURCE\tSEVERITY\tTYPE\tPATH\tCREATED")
			for _, r := range rows {
				p := ""
				if r.TargetPath != nil {
					p = *r.TargetPath
				}
				fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", r.ID, r.Source, r.Severity, r.EventType, p, r.CreatedAt.Format(time.RFC3339))
			}
			return tw.Flush()
		},
	}
	cmd.Flags().StringVar(&source, "source", "", "filter: maldet|clamav|yara")
	cmd.Flags().StringVar(&severity, "severity", "", "filter: info|warn|critical")
	cmd.Flags().StringVar(&user, "user", "", "filter by user id")
	return cmd
}

func newMalwareQuarantineCmd() *cobra.Command {
	cmd := &cobra.Command{Use: "quarantine", Short: "List / restore / delete quarantined files"}
	cmd.AddCommand(newMalwareQuarantineListCmd(), newMalwareQuarantineRestoreCmd(), newMalwareQuarantineDeleteCmd())
	return cmd
}

func newMalwareQuarantineListCmd() *cobra.Command {
	var limit int
	cmd := &cobra.Command{
		Use:     "list",
		Short:   "List active quarantine entries",
		Args:    cobra.NoArgs,
		PreRunE: requireDB,
		RunE: func(cmd *cobra.Command, _ []string) error {
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			rows, _, err := repository.NewMalwareQuarantineRepository(sharedDB).ListActive(ctx, limit, 0)
			if err != nil {
				return err
			}
			if jsonOutput {
				return printJSON(rows)
			}
			tw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
			fmt.Fprintln(tw, "ID\tSCANNER\tSIGNATURE\tORIGINAL_PATH\tDETECTED")
			for _, r := range rows {
				fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", r.ID, r.Scanner, r.Signature, r.OriginalPath, r.DetectedAt.Format(time.RFC3339))
			}
			return tw.Flush()
		},
	}
	cmd.Flags().IntVar(&limit, "limit", 100, "max rows")
	return cmd
}

func newMalwareQuarantineRestoreCmd() *cobra.Command {
	var reason string
	cmd := &cobra.Command{
		Use:     "restore <id>",
		Short:   "Restore a quarantined file to its original path",
		Args:    cobra.ExactArgs(1),
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, args []string) error {
			if reason == "" {
				return fmt.Errorf("--reason is required (restoring re-exposes a flagged file)")
			}
			ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
			defer cancel()
			repo := repository.NewMalwareQuarantineRepository(sharedDB)
			row, err := repo.Get(ctx, args[0])
			if err != nil {
				return fmt.Errorf("quarantine entry not found: %w", err)
			}
			if _, err := sharedAgent.Call(ctx, "security.malware.quarantine.restore", map[string]any{
				"quarantine_path": row.QuarantinePath,
				"original_path":   row.OriginalPath,
			}); err != nil {
				return err
			}
			if err := repo.Restore(ctx, row.ID, "cli", reason); err != nil {
				return fmt.Errorf("persist restore: %w", err)
			}
			cliAuditOK(ctx, "malware.quarantine_restore", "malware_quarantine", row.ID, row.UserID)
			fmt.Fprintf(cmd.OutOrStdout(), "Restored %s → %s\n", row.ID, row.OriginalPath)
			return nil
		},
	}
	cmd.Flags().StringVar(&reason, "reason", "", "why the file is being restored (required)")
	return cmd
}

func newMalwareQuarantineDeleteCmd() *cobra.Command {
	var force bool
	cmd := &cobra.Command{
		Use:     "delete <id>",
		Short:   "Permanently delete a quarantined file",
		Args:    cobra.ExactArgs(1),
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, args []string) error {
			if !force {
				return fmt.Errorf("deleting a quarantined file is irreversible; re-run with --force")
			}
			ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
			defer cancel()
			repo := repository.NewMalwareQuarantineRepository(sharedDB)
			row, err := repo.Get(ctx, args[0])
			if err != nil {
				return fmt.Errorf("quarantine entry not found: %w", err)
			}
			if _, err := sharedAgent.Call(ctx, "security.malware.quarantine.delete", map[string]any{
				"quarantine_path": row.QuarantinePath,
			}); err != nil {
				return err
			}
			if err := repo.MarkDeleted(ctx, row.ID); err != nil {
				return fmt.Errorf("persist delete: %w", err)
			}
			cliAuditOK(ctx, "malware.quarantine_delete", "malware_quarantine", row.ID, row.UserID)
			fmt.Fprintf(cmd.OutOrStdout(), "Deleted %s\n", row.ID)
			return nil
		},
	}
	cmd.Flags().BoolVar(&force, "force", false, "confirm permanent deletion")
	return cmd
}

func newMalwareYaraCmd() *cobra.Command {
	cmd := &cobra.Command{Use: "yara", Short: "Manage custom YARA rules"}
	cmd.AddCommand(
		csPassthroughCmd("list", "List custom YARA rules", "security.malware.yara.list"),
		newMalwareYaraUploadCmd(),
		newMalwareYaraToggleCmd(),
		newMalwareYaraDeleteCmd(),
	)
	return cmd
}

func newMalwareYaraUploadCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:     "upload <file.yar>",
		Short:   "Upload a custom YARA rule file",
		Args:    cobra.ExactArgs(1),
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, args []string) error {
			content, err := os.ReadFile(args[0])
			if err != nil {
				return fmt.Errorf("read rule file: %w", err)
			}
			filename := filepath.Base(args[0])
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			raw, err := sharedAgent.Call(ctx, "security.malware.yara.upload", map[string]any{
				"filename":       filename,
				"content_base64": base64.StdEncoding.EncodeToString(content),
			})
			if err != nil {
				return err
			}
			var resp struct {
				Filename string `json:"filename"`
				SHA256   string `json:"sha256"`
			}
			_ = json.Unmarshal(raw, &resp)
			if resp.Filename != "" {
				_ = repository.NewYARACustomRuleRepository(sharedDB).Upsert(ctx, &models.YARACustomRule{
					ID: ids.NewULID(), Filename: resp.Filename, Enabled: true, UploadedBy: "cli", SHA256: resp.SHA256,
				})
			}
			cliAuditOK(ctx, "malware.yara_upload", "yara_rule", filename, nil)
			os.Stdout.Write(raw)
			os.Stdout.Write([]byte{'\n'})
			return nil
		},
	}
	return cmd
}

func newMalwareYaraToggleCmd() *cobra.Command {
	var enable, disable bool
	cmd := &cobra.Command{
		Use:     "toggle <filename>",
		Short:   "Enable or disable a custom YARA rule",
		Args:    cobra.ExactArgs(1),
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, args []string) error {
			if enable == disable {
				return fmt.Errorf("pass exactly one of --enable or --disable")
			}
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			if _, err := sharedAgent.Call(ctx, "security.malware.yara.toggle", map[string]any{
				"filename": args[0], "enabled": enable,
			}); err != nil {
				return err
			}
			_ = repository.NewYARACustomRuleRepository(sharedDB).SetEnabled(ctx, args[0], enable)
			cliAuditOK(ctx, "malware.yara_toggle", "yara_rule", args[0], nil)
			fmt.Fprintf(cmd.OutOrStdout(), "%s enabled=%v\n", args[0], enable)
			return nil
		},
	}
	cmd.Flags().BoolVar(&enable, "enable", false, "enable the rule")
	cmd.Flags().BoolVar(&disable, "disable", false, "disable the rule")
	return cmd
}

func newMalwareYaraDeleteCmd() *cobra.Command {
	return &cobra.Command{
		Use:     "delete <filename>",
		Short:   "Delete a custom YARA rule",
		Args:    cobra.ExactArgs(1),
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, args []string) error {
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			err := csCall(ctx, "security.malware.yara.delete", map[string]any{"filename": args[0]})
			if err == nil {
				cliAuditOK(ctx, "malware.yara_delete", "yara_rule", args[0], nil)
			}
			return err
		},
	}
}

func newMalwareSettingsCmd() *cobra.Command {
	cmd := &cobra.Command{Use: "settings", Short: "Get / set malware settings"}
	cmd.AddCommand(
		&cobra.Command{
			Use: "get", Short: "Show malware settings", Args: cobra.NoArgs, PreRunE: requireDB,
			RunE: func(cmd *cobra.Command, _ []string) error {
				ctx, cancel := context.WithTimeout(cmd.Context(), 15*time.Second)
				defer cancel()
				repo := repository.NewMalwareSettingsRepository(sharedDB)
				_ = repo.EnsureDefault(ctx)
				s, err := repo.Get(ctx)
				if err != nil {
					return err
				}
				return printJSON(s)
			},
		},
		newMalwareSettingsSetCmd(),
	)
	return cmd
}

func newMalwareSettingsSetCmd() *cobra.Command {
	var (
		retainDays      int
		maxScanMB       int
		notifyThreshold string
		realtime        string
	)
	cmd := &cobra.Command{
		Use:     "set",
		Short:   "Update malware settings (same validation as the UI)",
		Args:    cobra.NoArgs,
		PreRunE: requireDBAndAgent,
		RunE: func(cmd *cobra.Command, _ []string) error {
			ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
			defer cancel()
			repo := repository.NewMalwareSettingsRepository(sharedDB)
			_ = repo.EnsureDefault(ctx)
			s, err := repo.Get(ctx)
			if err != nil {
				return err
			}
			prevRealtime := s.RealtimeEnabled
			if cmd.Flags().Changed("retain-days") {
				if retainDays < 1 || retainDays > 365 {
					return fmt.Errorf("--retain-days must be 1..365")
				}
				s.QuarantineRetainDays = uint(retainDays)
			}
			if cmd.Flags().Changed("max-scan-mb") {
				if maxScanMB < 1 || maxScanMB > 10240 {
					return fmt.Errorf("--max-scan-mb must be 1..10240")
				}
				s.MaxScanMB = uint(maxScanMB)
			}
			if cmd.Flags().Changed("notify-threshold") {
				switch notifyThreshold {
				case models.MalwareSeverityInfo, models.MalwareSeverityWarn, models.MalwareSeverityCritical:
					s.NotifyThreshold = notifyThreshold
				default:
					return fmt.Errorf("--notify-threshold must be info|warn|critical")
				}
			}
			if cmd.Flags().Changed("realtime") {
				switch realtime {
				case "on", "true":
					s.RealtimeEnabled = true
				case "off", "false":
					s.RealtimeEnabled = false
				default:
					return fmt.Errorf("--realtime must be on|off")
				}
			}
			// Apply realtime to the host BEFORE persisting (apply-then-persist).
			if s.RealtimeEnabled != prevRealtime {
				if _, err := sharedAgent.Call(ctx, "security.malware.monitor.set_enabled", map[string]any{
					"enabled": s.RealtimeEnabled,
				}); err != nil {
					return fmt.Errorf("apply realtime monitor: %w", err)
				}
			}
			if err := repo.Upsert(ctx, s); err != nil {
				return fmt.Errorf("persist settings: %w", err)
			}
			cliAuditOK(ctx, "malware.settings_update", "malware_settings", "1", nil)
			return printJSON(s)
		},
	}
	cmd.Flags().IntVar(&retainDays, "retain-days", 0, "quarantine retention days (1..365)")
	cmd.Flags().IntVar(&maxScanMB, "max-scan-mb", 0, "max scan file size MB (1..10240)")
	cmd.Flags().StringVar(&notifyThreshold, "notify-threshold", "", "info|warn|critical")
	cmd.Flags().StringVar(&realtime, "realtime", "", "on|off (real-time monitor)")
	return cmd
}
