// Package udp manages user-defined UDP integrations: inbound listeners
// (WSJT-X, JTDX, MSHV log events; JTAlert ADIF; N1MM XML; DXHunter call)
// and outbound emitters (db_updated → notifies Cloudlog/N1MM when HamLog
// just logged a QSO).
//
// One Server per connection row, started/stopped by the Manager when the
// user enables/disables or edits the row. Multicast support lets multiple
// apps share the same port without bind conflicts — essential since
// WSJT-X uses 2237 and several tools already listen there.
package udp

import (
	"context"
	"database/sql"
	"fmt"

	"hamlog/internal/db"
)

// Direction is "inbound" (we listen) or "outbound" (we emit).
type Direction string

const (
	Inbound  Direction = "inbound"
	Outbound Direction = "outbound"
)

// ServiceType selects the parser/emitter for a connection.
type ServiceType string

const (
	ServiceWSJT       ServiceType = "wsjt"        // WSJT-X / JTDX / MSHV binary (inbound)
	ServiceADIF       ServiceType = "adif"        // text ADIF over UDP (inbound)
	ServiceN1MM       ServiceType = "n1mm"        // N1MM Logger+ XML (inbound)
	ServiceRemoteCall ServiceType = "remote_call" // plain text callsign (inbound)
	// Outbound emitters.
	ServiceDBUpdated ServiceType = "db_updated"      // ADIF of each locally-logged QSO (on save)
	ServicePstFreq   ServiceType = "pstrotator_freq" // <PST><FREQUENCY> radio freq (on freq change)
	ServiceN1MMRadio ServiceType = "n1mm_radioinfo"  // N1MM RadioInfo XML: freq+mode (on freq/mode change)
	// ServiceWSJTRelay re-sends every datagram an inbound WSJT listener receives,
	// byte for byte, to somewhere else.
	//
	// WSJT-X, JTDX and MSHV send to ONE address. Anything else that wants the
	// same stream — JTAlert, GridTracker, a second logger — has to be fed by a
	// relay, and this is it, so OpsLog stops being the reason you cannot run the
	// two together.
	//
	// VERBATIM, with no origin header prepended. The relays in the field add one
	// ("127.0.0.1:2237|"), which is precisely why stripForwarderHeader exists on
	// the receiving side here; inflicting the same thing on whatever is
	// downstream would be repeating a mistake we had to write code to survive.
	ServiceWSJTRelay ServiceType = "wsjt_relay"
	// ServiceWSJTLog wraps the same ADIF in a WSJT-X "Logged ADIF" datagram.
	// Logger32 and others listen on the WSJT-X interface, not for plain text, and
	// discard a bare ADIF record without a word — so the two cannot be one row.
	ServiceWSJTLog ServiceType = "wsjt_log"
	// ServiceCustom is the general case: the operator picks the trigger, writes
	// the message, and chooses UDP or HTTP. See Trigger and Config.Template.
	ServiceCustom ServiceType = "custom"
)

// Trigger names the moment a custom outbound row fires.
type Trigger string

const (
	TriggerQSOLogged  Trigger = "qso_logged"
	TriggerRotatorGo  Trigger = "rotator_goto"
	TriggerLookupDone Trigger = "lookup_done"
	// TriggerBandChange is the RIG changing band, not the entry form: a switch
	// follows the radio, not what is being typed. Rare by nature, so unlike a
	// frequency trigger it needs no damping.
	TriggerBandChange Trigger = "band_change"
)

// Transport is how a custom row leaves: a UDP datagram or an HTTP GET.
type Transport string

const (
	TransportUDP Transport = "udp"
	TransportURL Transport = "url"
)

// Config is one user-defined UDP connection.
type Config struct {
	ID             int64       `json:"id"`
	Direction      Direction   `json:"direction"`
	Name           string      `json:"name"`
	Port           int         `json:"port"`
	ServiceType    ServiceType `json:"service_type"`
	Multicast      bool        `json:"multicast"`
	MulticastGroup string      `json:"multicast_group"`
	DestinationIP  string      `json:"destination_ip"` // outbound only
	Enabled        bool        `json:"enabled"`
	SortOrder      int         `json:"sort_order"`

	// Custom rows only (ServiceCustom).
	Trigger   Trigger   `json:"trigger"`
	Template  string    `json:"template"`  // message text with {fields}
	Transport Transport `json:"transport"` // udp | url
	URL       string    `json:"url"`       // URL template, when transport is url
	LineEnd   string    `json:"line_end"`  // "" | lf | crlf — UDP only
}

// Repo is the persistence layer for UDP integration rows.
type Repo struct{ db *sql.DB }

func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }

func (r *Repo) List(ctx context.Context) ([]Config, error) {
	rows, err := r.db.QueryContext(ctx, `
		SELECT id, direction, name, port, service_type,
		       multicast, multicast_group, destination_ip,
		       enabled, sort_order,
		       trig, template, transport, url, line_end
		FROM integrations_udp
		ORDER BY direction, sort_order, id`)
	if err != nil {
		return nil, fmt.Errorf("list udp: %w", err)
	}
	defer rows.Close()
	var out []Config
	for rows.Next() {
		var c Config
		var mc, en int
		if err := rows.Scan(&c.ID, &c.Direction, &c.Name, &c.Port, &c.ServiceType,
			&mc, &c.MulticastGroup, &c.DestinationIP, &en, &c.SortOrder,
			&c.Trigger, &c.Template, &c.Transport, &c.URL, &c.LineEnd); err != nil {
			return nil, err
		}
		c.Multicast = mc != 0
		c.Enabled = en != 0
		out = append(out, c)
	}
	return out, rows.Err()
}

func (r *Repo) Save(ctx context.Context, c *Config) error {
	if c.Direction != Inbound && c.Direction != Outbound {
		return fmt.Errorf("invalid direction %q", c.Direction)
	}
	if c.Name == "" {
		return fmt.Errorf("name required")
	}
	mc, en := 0, 0
	if c.Multicast {
		mc = 1
	}
	if c.Enabled {
		en = 1
	}
	if c.ID == 0 {
		res, err := r.db.ExecContext(ctx, `
			INSERT INTO integrations_udp(direction, name, port, service_type,
			    multicast, multicast_group, destination_ip, enabled, sort_order,
			    trig, template, transport, url, line_end)
			VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			c.Direction, c.Name, c.Port, c.ServiceType,
			mc, c.MulticastGroup, c.DestinationIP, en, c.SortOrder,
			c.Trigger, c.Template, c.Transport, c.URL, c.LineEnd)
		if err != nil {
			return fmt.Errorf("insert udp: %w", err)
		}
		id, _ := res.LastInsertId()
		c.ID = id
		return nil
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE integrations_udp SET
		  direction = ?, name = ?, port = ?, service_type = ?,
		  multicast = ?, multicast_group = ?, destination_ip = ?,
		  enabled = ?, sort_order = ?,
		  trig = ?, template = ?, transport = ?, url = ?, line_end = ?,
		  updated_at = ?
		WHERE id = ?`,
		c.Direction, c.Name, c.Port, c.ServiceType,
		mc, c.MulticastGroup, c.DestinationIP, en, c.SortOrder,
		c.Trigger, c.Template, c.Transport, c.URL, c.LineEnd, db.NowISO(), c.ID)
	if err != nil {
		return fmt.Errorf("update udp: %w", err)
	}
	return nil
}

func (r *Repo) Delete(ctx context.Context, id int64) error {
	_, err := r.db.ExecContext(ctx, `DELETE FROM integrations_udp WHERE id = ?`, id)
	return err
}
