package db

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/jackc/pgx/v5"
)

// InstanceRow is the persisted form of an instance record.
type InstanceRow struct {
	InstanceID    string
	AgentID       string
	ModuleID      string
	ModuleVersion string
	RunMode       string
	DataPath      string
	ConfigValues  map[string]string
	// AssignedPorts maps manifest port name → host port the panel
	// allocated. Persisted so stopped instances still reserve their
	// slots — net.Listen probing can't see ports for instances that
	// aren't currently running, and would otherwise hand the same
	// port to a new instance.
	AssignedPorts map[string]uint32
	Status        string // 'stopped' | 'starting' | 'running' | 'stopping' | 'crashed' | 'updating'
	LastExitCode  int32
	Detail        string
	CreatedAt     time.Time
	UpdatedAt     time.Time
}

// UpsertInstance inserts or updates an instance by instance_id. Used from
// CreateInstance — it's idempotent so retry is safe.
func (db *DB) UpsertInstance(ctx context.Context, r InstanceRow) error {
	cfgJSON, err := json.Marshal(r.ConfigValues)
	if err != nil {
		return fmt.Errorf("marshal config_values: %w", err)
	}
	if r.AssignedPorts == nil {
		r.AssignedPorts = map[string]uint32{}
	}
	portsJSON, err := json.Marshal(r.AssignedPorts)
	if err != nil {
		return fmt.Errorf("marshal assigned_ports: %w", err)
	}
	_, err = db.pool.Exec(ctx, `
		INSERT INTO instances
			(instance_id, agent_id, module_id, module_version, run_mode, data_path, config_values, assigned_ports, status)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		ON CONFLICT (instance_id) DO UPDATE SET
			agent_id       = EXCLUDED.agent_id,
			module_id      = EXCLUDED.module_id,
			module_version = EXCLUDED.module_version,
			run_mode       = EXCLUDED.run_mode,
			data_path      = EXCLUDED.data_path,
			config_values  = EXCLUDED.config_values,
			assigned_ports = EXCLUDED.assigned_ports,
			status         = EXCLUDED.status,
			updated_at     = NOW()
	`, r.InstanceID, r.AgentID, r.ModuleID, r.ModuleVersion, r.RunMode, r.DataPath, cfgJSON, portsJSON, r.Status)
	if err != nil {
		return fmt.Errorf("upsert instance: %w", err)
	}
	return nil
}

// UpdateInstanceConfigValues replaces ONLY the config_values column. Used by
// the durable config-render path, which must not touch status / data_path /
// assigned_ports the way the full UpsertInstance does (a config save racing
// an instance start could otherwise stomp a live status transition).
func (db *DB) UpdateInstanceConfigValues(ctx context.Context, instanceID string, values map[string]string) error {
	cfgJSON, err := json.Marshal(values)
	if err != nil {
		return fmt.Errorf("marshal config_values: %w", err)
	}
	_, err = db.pool.Exec(ctx, `
		UPDATE instances SET config_values = $2, updated_at = NOW()
		WHERE instance_id = $1
	`, instanceID, cfgJSON)
	if err != nil {
		return fmt.Errorf("update instance config_values: %w", err)
	}
	return nil
}

// UpdateInstanceStatus records a status transition from an InstanceStateUpdate.
// updated_at is only bumped when the status actually changed — re-announces
// from agent reconnects (e.g. controller restart triggering an announce loop)
// would otherwise reset the timestamp every time, which breaks the frontend's
// "instance has been running for >10 min" heuristic for clearing the
// "starting" overlay on log-only modules.
func (db *DB) UpdateInstanceStatus(ctx context.Context, instanceID, status string, exitCode int32, detail string) error {
	_, err := db.pool.Exec(ctx, `
		UPDATE instances
		SET status = $2,
			last_exit_code = $3,
			detail = $4,
			updated_at = CASE WHEN status IS DISTINCT FROM $2 THEN NOW() ELSE updated_at END
		WHERE instance_id = $1
	`, instanceID, status, exitCode, detail)
	if err != nil {
		return fmt.Errorf("update instance status: %w", err)
	}
	return nil
}

// DeleteInstance is not yet used; included so the schema doesn't grow a
// parallel soft-delete pattern before we need one. Call when an operator
// permanently removes an instance.
func (db *DB) DeleteInstance(ctx context.Context, instanceID string) error {
	_, err := db.pool.Exec(ctx, `DELETE FROM instances WHERE instance_id = $1`, instanceID)
	if err != nil {
		return fmt.Errorf("delete instance: %w", err)
	}
	return nil
}

// GetInstanceRow returns one instance row by ID, or an error if missing.
func (db *DB) GetInstanceRow(ctx context.Context, instanceID string) (InstanceRow, error) {
	const q = `
		SELECT instance_id, agent_id, module_id, COALESCE(module_version, ''),
		       run_mode, COALESCE(data_path, ''), config_values, assigned_ports, status,
		       COALESCE(last_exit_code, 0), COALESCE(detail, ''),
		       created_at, updated_at
		FROM instances WHERE instance_id = $1
	`
	var r InstanceRow
	var cfgJSON, portsJSON []byte
	err := db.pool.QueryRow(ctx, q, instanceID).Scan(
		&r.InstanceID, &r.AgentID, &r.ModuleID, &r.ModuleVersion,
		&r.RunMode, &r.DataPath, &cfgJSON, &portsJSON, &r.Status,
		&r.LastExitCode, &r.Detail,
		&r.CreatedAt, &r.UpdatedAt,
	)
	if err != nil {
		return r, fmt.Errorf("get instance %q: %w", instanceID, err)
	}
	if len(cfgJSON) > 0 {
		_ = json.Unmarshal(cfgJSON, &r.ConfigValues)
	}
	if len(portsJSON) > 0 {
		_ = json.Unmarshal(portsJSON, &r.AssignedPorts)
	}
	return r, nil
}

// ListInstances returns all instances, optionally filtered by agent_id.
func (db *DB) ListInstances(ctx context.Context, agentFilter string) ([]InstanceRow, error) {
	var rows pgx.Rows
	var err error
	const selectAll = `
		SELECT instance_id, agent_id, module_id, COALESCE(module_version, ''),
		       run_mode, COALESCE(data_path, ''), config_values, assigned_ports, status,
		       COALESCE(last_exit_code, 0), COALESCE(detail, ''),
		       created_at, updated_at
		FROM instances
	`
	if agentFilter != "" {
		rows, err = db.pool.Query(ctx, selectAll+` WHERE agent_id = $1 ORDER BY instance_id`, agentFilter)
	} else {
		rows, err = db.pool.Query(ctx, selectAll+` ORDER BY instance_id`)
	}
	if err != nil {
		return nil, fmt.Errorf("list instances: %w", err)
	}
	defer rows.Close()

	var out []InstanceRow
	for rows.Next() {
		var r InstanceRow
		var cfgJSON, portsJSON []byte
		if err := rows.Scan(
			&r.InstanceID, &r.AgentID, &r.ModuleID, &r.ModuleVersion,
			&r.RunMode, &r.DataPath, &cfgJSON, &portsJSON, &r.Status,
			&r.LastExitCode, &r.Detail,
			&r.CreatedAt, &r.UpdatedAt,
		); err != nil {
			return nil, fmt.Errorf("scan instance: %w", err)
		}
		if len(cfgJSON) > 0 {
			_ = json.Unmarshal(cfgJSON, &r.ConfigValues)
		}
		if len(portsJSON) > 0 {
			_ = json.Unmarshal(portsJSON, &r.AssignedPorts)
		}
		out = append(out, r)
	}
	return out, rows.Err()
}
