package db

import (
	"context"
	"fmt"
	"time"
)

// AgentRow is the persisted form of an agent record.
type AgentRow struct {
	AgentID   string
	Version   string
	HostOS    string
	HostArch  string
	Hostname  string
	Label     string
	Notes     string
	// PortRangeStart/End define the inclusive host-port window the panel
	// allocates from when creating new instances on this agent. Both 0
	// (unset) means use the global default 7000-9999. Set via the agent
	// edit modal so operators can isolate per-agent port pools (e.g.
	// agent A: 7000-7999, agent B: 8000-8999) for firewall sanity.
	PortRangeStart int32
	PortRangeEnd   int32
	// LANIP is the agent's RFC1918 LAN IP — used as the `target` field
	// when the opnfwd integration creates a port forward for an instance
	// on this agent. Empty disables the integration for this agent (loud
	// log, no-op). Operator-edited via the agent edit modal.
	LANIP     string
	FirstSeen time.Time
	LastSeen  time.Time
}

// UpsertAgent inserts the agent or updates its metadata on reconnect,
// bumping last_seen. first_seen is preserved on conflict.
func (db *DB) UpsertAgent(ctx context.Context, a AgentRow) error {
	_, err := db.pool.Exec(ctx, `
		INSERT INTO agents (agent_id, version, host_os, host_arch, hostname, first_seen, last_seen)
		VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
		ON CONFLICT (agent_id) DO UPDATE SET
			version = EXCLUDED.version,
			host_os = EXCLUDED.host_os,
			host_arch = EXCLUDED.host_arch,
			hostname = EXCLUDED.hostname,
			last_seen = NOW()
	`, a.AgentID, a.Version, a.HostOS, a.HostArch, a.Hostname)
	if err != nil {
		return fmt.Errorf("upsert agent: %w", err)
	}
	return nil
}

// TouchAgent bumps last_seen for an agent. Used on heartbeat arrival.
func (db *DB) TouchAgent(ctx context.Context, agentID string) error {
	_, err := db.pool.Exec(ctx, `UPDATE agents SET last_seen = NOW() WHERE agent_id = $1`, agentID)
	if err != nil {
		return fmt.Errorf("touch agent: %w", err)
	}
	return nil
}

// UpdateAgentMeta sets operator-assigned display fields.
func (db *DB) UpdateAgentMeta(ctx context.Context, agentID, label, notes string) error {
	_, err := db.pool.Exec(ctx, `UPDATE agents SET label = $2, notes = $3 WHERE agent_id = $1`, agentID, label, notes)
	if err != nil {
		return fmt.Errorf("update agent meta: %w", err)
	}
	return nil
}

// SetAgentPortRange persists the host-port window this agent allocates
// new instances from. Pass 0,0 to clear (allocator falls back to global
// default). Validates that start <= end and both fit in uint16 range.
func (db *DB) SetAgentPortRange(ctx context.Context, agentID string, start, end int32) error {
	if start == 0 && end == 0 {
		_, err := db.pool.Exec(ctx, `UPDATE agents SET port_range_start = NULL, port_range_end = NULL WHERE agent_id = $1`, agentID)
		if err != nil {
			return fmt.Errorf("clear port range: %w", err)
		}
		return nil
	}
	if start <= 0 || end <= 0 || start > 65535 || end > 65535 || start > end {
		return fmt.Errorf("invalid port range %d-%d (must be 1-65535 and start<=end)", start, end)
	}
	_, err := db.pool.Exec(ctx, `UPDATE agents SET port_range_start = $2, port_range_end = $3 WHERE agent_id = $1`, agentID, start, end)
	if err != nil {
		return fmt.Errorf("set port range: %w", err)
	}
	return nil
}

// GetAgentPortRange returns the configured window or (0,0) if unset.
// (0,0) means "use the global default" — allocator decides what.
func (db *DB) GetAgentPortRange(ctx context.Context, agentID string) (start, end int32, err error) {
	var s, e *int32
	err = db.pool.QueryRow(ctx, `SELECT port_range_start, port_range_end FROM agents WHERE agent_id = $1`, agentID).Scan(&s, &e)
	if err != nil {
		return 0, 0, err
	}
	if s != nil {
		start = *s
	}
	if e != nil {
		end = *e
	}
	return
}

// GetAgentMeta returns just the label + notes (avoids pulling the full row
// when we already have everything else from the in-memory agent registry).
func (db *DB) GetAgentMeta(ctx context.Context, agentID string) (label, notes string, err error) {
	err = db.pool.QueryRow(ctx, `SELECT COALESCE(label, ''), COALESCE(notes, '') FROM agents WHERE agent_id = $1`, agentID).Scan(&label, &notes)
	return
}

// GetAgentLANIP returns the operator-set LAN IP for this agent. Empty
// string means the operator hasn't filled it in — opnfwd integration
// no-ops for instances on this agent until they do.
func (db *DB) GetAgentLANIP(ctx context.Context, agentID string) (string, error) {
	var v string
	err := db.pool.QueryRow(ctx, `SELECT COALESCE(lan_ip, '') FROM agents WHERE agent_id = $1`, agentID).Scan(&v)
	return v, err
}

// SetAgentLANIP persists the LAN IP. Empty string is allowed (clears).
// Caller is responsible for sanity-checking the IP format.
func (db *DB) SetAgentLANIP(ctx context.Context, agentID, ip string) error {
	_, err := db.pool.Exec(ctx, `UPDATE agents SET lan_ip = $2 WHERE agent_id = $1`, agentID, ip)
	if err != nil {
		return fmt.Errorf("set agent lan_ip: %w", err)
	}
	return nil
}

// DeleteAgent removes an agent record. The agent's cert remains valid
// until expiry — true revocation would require a CRL; for MVP this is a
// forget-from-DB. If the agent is live it'll just re-register next beat.
func (db *DB) DeleteAgent(ctx context.Context, agentID string) error {
	_, err := db.pool.Exec(ctx, `DELETE FROM agents WHERE agent_id = $1`, agentID)
	if err != nil {
		return fmt.Errorf("delete agent: %w", err)
	}
	return nil
}

// ListAgents returns every row in the agents table. The in-memory registry
// only knows about live-connected agents, but the dashboard needs to surface
// offline ones too so instances owned by an offline agent don't silently
// disappear from the UI. Caller merges live + DB and marks the DB-only ones
// as offline.
func (db *DB) ListAgents(ctx context.Context) ([]AgentRow, error) {
	rows, err := db.pool.Query(ctx, `
		SELECT agent_id, version, host_os, host_arch, hostname,
		       COALESCE(label, ''), COALESCE(notes, ''),
		       COALESCE(port_range_start, 0), COALESCE(port_range_end, 0),
		       COALESCE(lan_ip, ''),
		       first_seen, last_seen
		FROM agents
		ORDER BY agent_id`)
	if err != nil {
		return nil, fmt.Errorf("list agents: %w", err)
	}
	defer rows.Close()
	var out []AgentRow
	for rows.Next() {
		var a AgentRow
		if err := rows.Scan(&a.AgentID, &a.Version, &a.HostOS, &a.HostArch, &a.Hostname,
			&a.Label, &a.Notes, &a.PortRangeStart, &a.PortRangeEnd, &a.LANIP,
			&a.FirstSeen, &a.LastSeen); err != nil {
			return nil, fmt.Errorf("scan agent row: %w", err)
		}
		out = append(out, a)
	}
	return out, rows.Err()
}
