package model

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"
)

const (
	DefaultBridgeName               = "br-fc"
	DefaultBridgeIP                 = "172.16.0.1"
	DefaultCIDR                     = "24"
	DefaultDNS                      = "1.1.1.1"
	DefaultSystemOverlaySize        = 8 * 1024 * 1024 * 1024
	DefaultWorkDiskSize             = 8 * 1024 * 1024 * 1024
	DefaultMemoryMiB                = 2048
	DefaultVCPUCount                = 2
	DefaultStatsPollInterval        = 10 * time.Second
	DefaultStaleSweepInterval       = 1 * time.Minute
	MaxDiskBytes              int64 = 128 * 1024 * 1024 * 1024
	DefaultJailerBinary             = "/usr/bin/jailer"
)

type VMState string

const (
	VMStateCreated VMState = "created"
	VMStateRunning VMState = "running"
	VMStateStopped VMState = "stopped"
	VMStateError   VMState = "error"
)

type DaemonConfig struct {
	LogLevel           string
	FirecrackerBin     string
	JailerBin          string
	JailerEnabled      bool
	JailerChrootBase   string
	SSHKeyPath         string
	HostHomeDir        string
	AutoStopStaleAfter time.Duration
	StatsPollInterval  time.Duration
	BridgeName         string
	BridgeIP           string
	CIDR               string
	TapPoolSize        int
	DefaultDNS         string
	DefaultImageName   string
	FileSync           []FileSyncEntry
	VMDefaults         VMDefaultsOverride
}

// FileSyncEntry is a user-declared host→guest file or directory copy
// applied to each VM's work disk at vm create time. Host is expanded
// against the configured owner home for "~/..." and must stay within
// that home; Guest is expanded against /root (banger VMs are
// single-user root). If the host path is a directory, it's copied
// recursively; if it's a file, it's copied as a file. Missing host
// paths are a soft skip (warned, not fatal). Mode defaults to 0600
// for files and 0755 for directories.
type FileSyncEntry struct {
	Host  string
	Guest string
	Mode  string
}

type Image struct {
	ID                            string    `json:"id"`
	Name                          string    `json:"name"`
	Managed                       bool      `json:"managed"`
	ArtifactDir                   string    `json:"artifact_dir,omitempty"`
	RootfsPath                    string    `json:"rootfs_path"`
	WorkSeedPath                  string    `json:"work_seed_path,omitempty"`
	KernelPath                    string    `json:"kernel_path"`
	InitrdPath                    string    `json:"initrd_path,omitempty"`
	ModulesDir                    string    `json:"modules_dir,omitempty"`
	BuildSize                     string    `json:"build_size,omitempty"`
	SeededSSHPublicKeyFingerprint string    `json:"seeded_ssh_public_key_fingerprint,omitempty"`
	CreatedAt                     time.Time `json:"created_at"`
	UpdatedAt                     time.Time `json:"updated_at"`
}

type VMSpec struct {
	VCPUCount             int   `json:"vcpu_count"`
	MemoryMiB             int   `json:"memory_mib"`
	SystemOverlaySizeByte int64 `json:"system_overlay_size_bytes"`
	WorkDiskSizeBytes     int64 `json:"work_disk_size_bytes"`
	NATEnabled            bool  `json:"nat_enabled"`
}

// VMRuntime holds the durable runtime state that the daemon needs
// to reach a VM: identity, declared state, and deterministic derived
// paths. The authoritative live handle set still lives on VMHandles,
// but teardown-critical storage/network identifiers are mirrored here
// as recovery fallbacks so restart-time cleanup still works when
// handles.json is missing or corrupt.
//
// Everything in VMRuntime is safe to persist: the paths are
// deterministic from (VM ID, layout) and survive restart unchanged;
// GuestIP and DNSName are assigned at create time and never move;
// LastError carries the last failure message for debugging. State
// mirrors VMRecord.State.
type VMRuntime struct {
	State       VMState `json:"state"`
	GuestIP     string  `json:"guest_ip"`
	APISockPath string  `json:"api_sock_path,omitempty"`
	VSockPath   string  `json:"vsock_path,omitempty"`
	VSockCID    uint32  `json:"vsock_cid,omitempty"`
	LogPath     string  `json:"log_path,omitempty"`
	MetricsPath string  `json:"metrics_path,omitempty"`
	DNSName     string  `json:"dns_name,omitempty"`
	VMDir       string  `json:"vm_dir"`
	// Teardown fallback fields mirror the handle cache onto the VM row.
	// They are recovery-only: while the daemon is alive, VMHandles stays
	// authoritative. On restart, cleanup can fall back to these values if
	// handles.json is missing or corrupt.
	TapDevice     string `json:"tap_device,omitempty"`
	BaseLoop      string `json:"base_loop,omitempty"`
	COWLoop       string `json:"cow_loop,omitempty"`
	DMName        string `json:"dm_name,omitempty"`
	DMDev         string `json:"dm_dev,omitempty"`
	SystemOverlay string `json:"system_overlay_path"`
	WorkDiskPath  string `json:"work_disk_path"`
	LastError     string `json:"last_error,omitempty"`
}

type VMStats struct {
	CollectedAt        time.Time      `json:"collected_at,omitempty"`
	CPUPercent         float64        `json:"cpu_percent,omitempty"`
	RSSBytes           int64          `json:"rss_bytes,omitempty"`
	VSZBytes           int64          `json:"vsz_bytes,omitempty"`
	SystemOverlayBytes int64          `json:"system_overlay_bytes,omitempty"`
	WorkDiskBytes      int64          `json:"work_disk_bytes,omitempty"`
	MetricsRaw         map[string]any `json:"metrics_raw,omitempty"`
}

type VMRecord struct {
	ID            string      `json:"id"`
	Name          string      `json:"name"`
	ImageID       string      `json:"image_id"`
	State         VMState     `json:"state"`
	CreatedAt     time.Time   `json:"created_at"`
	UpdatedAt     time.Time   `json:"updated_at"`
	LastTouchedAt time.Time   `json:"last_touched_at"`
	Spec          VMSpec      `json:"spec"`
	Runtime       VMRuntime   `json:"runtime"`
	Stats         VMStats     `json:"stats"`
	Workspace     VMWorkspace `json:"workspace"`
}

type VMCreateRequest struct {
	Name                  string
	ImageName             string
	VCPUCount             int
	MemoryMiB             int
	SystemOverlaySizeByte int64
	WorkDiskSizeBytes     int64
	NATEnabled            bool
	NoStart               bool
}

type VMSetRequest struct {
	IDOrName          string
	VCPUCount         *int
	MemoryMiB         *int
	WorkDiskSizeBytes *int64
	NATEnabled        *bool
}

// VMWorkspace records the last successful workspace.prepare result on
// a VM so callers can skip re-stating the source path on every exec
// and so banger can detect drift between the guest copy and the host
// repo. Stored as workspace_json in the vms table; zero value means
// no workspace has been prepared on this VM yet.
type VMWorkspace struct {
	GuestPath  string    `json:"guest_path,omitempty"`
	SourcePath string    `json:"source_path,omitempty"`
	HeadCommit string    `json:"head_commit,omitempty"`
	PreparedAt time.Time `json:"prepared_at,omitempty"`
}

type WorkspacePrepareMode string

const (
	WorkspacePrepareModeShallowOverlay WorkspacePrepareMode = "shallow_overlay"
	WorkspacePrepareModeFullCopy       WorkspacePrepareMode = "full_copy"
	WorkspacePrepareModeMetadataOnly   WorkspacePrepareMode = "metadata_only"
)

type WorkspacePrepareResult struct {
	VMID          string               `json:"vm_id"`
	SourcePath    string               `json:"source_path"`
	RepoRoot      string               `json:"repo_root"`
	RepoName      string               `json:"repo_name"`
	GuestPath     string               `json:"guest_path"`
	Mode          WorkspacePrepareMode `json:"mode"`
	HeadCommit    string               `json:"head_commit,omitempty"`
	CurrentBranch string               `json:"current_branch,omitempty"`
	BranchName    string               `json:"branch_name,omitempty"`
	BaseCommit    string               `json:"base_commit,omitempty"`
	PreparedAt    time.Time            `json:"prepared_at"`
}

func Now() time.Time {
	return time.Now().UTC().Truncate(time.Second)
}

func NewID() (string, error) {
	buf := make([]byte, 32)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return hex.EncodeToString(buf), nil
}

// NewOpID returns a short identifier for tracing a single RPC
// operation across the daemon, the root helper, and the user-visible
// CLI error string. Format: "op-" + 12 hex chars (48 bits of entropy
// — collisions inside one daemon session are vanishingly unlikely
// and don't matter beyond it). Short enough to copy-paste from a
// CLI error into a journalctl --grep, long enough to actually
// disambiguate.
func NewOpID() (string, error) {
	buf := make([]byte, 6)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return "op-" + hex.EncodeToString(buf), nil
}

func ParseSize(raw string) (int64, error) {
	if raw == "" {
		return 0, errors.New("size is required")
	}
	raw = strings.TrimSpace(strings.ToUpper(raw))
	if raw == "" {
		return 0, errors.New("size is required")
	}
	// Strip an optional "IB" suffix so that "GiB", "MiB", "KiB" work the
	// same as "G", "M", "K" (case-insensitive after ToUpper).
	number := strings.TrimSuffix(raw, "IB")
	unit := number[len(number)-1]
	multiplier := int64(1024 * 1024)
	switch unit {
	case 'K':
		multiplier = 1024
		number = number[:len(number)-1]
	case 'M':
		multiplier = 1024 * 1024
		number = number[:len(number)-1]
	case 'G':
		multiplier = 1024 * 1024 * 1024
		number = number[:len(number)-1]
	default:
		if unit < '0' || unit > '9' {
			return 0, fmt.Errorf("unsupported size suffix: %q", string(unit))
		}
		number = raw // no suffix stripped — keep original digits-only string
	}
	value, err := strconv.ParseInt(number, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("parse size %q: %w", raw, err)
	}
	result := value * multiplier
	if result <= 0 {
		return 0, fmt.Errorf("size must be positive: %q", raw)
	}
	if result > MaxDiskBytes {
		return 0, fmt.Errorf("size exceeds max of %d bytes", MaxDiskBytes)
	}
	return result, nil
}

func FormatSizeBytes(bytes int64) string {
	switch {
	case bytes%(1024*1024*1024) == 0:
		return fmt.Sprintf("%dG", bytes/(1024*1024*1024))
	case bytes%(1024*1024) == 0:
		return fmt.Sprintf("%dM", bytes/(1024*1024))
	case bytes%1024 == 0:
		return fmt.Sprintf("%dK", bytes/1024)
	default:
		return strconv.FormatInt(bytes, 10)
	}
}
