package udp

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"hamlog/internal/applog"
)

// Custom outbound messages: a trigger, a template, a transport.
//
// The hard-coded emitters beside this one each speak a published format at a
// fixed moment. This is the escape hatch: the operator chooses when it fires and
// writes what it says. It exists mainly for antenna switches, which are driven
// by a URL — http://192.168.1.50/antenna?band=20m — and want the band change,
// but nothing here is specific to them.
//
// The presets are NOT reimplemented on top of this. PstRotator and N1MM
// RadioInfo are documented formats that work; a typo in a template would break
// them silently, and "it worked before I touched anything" is not a trade worth
// making for one less code path.

// httpTimeout bounds a request. A UDP datagram leaves and is forgotten; an HTTP
// call to a switch that has been unplugged will otherwise sit there for the
// operating system's own timeout — and this fires on a band change, which is
// exactly when nobody wants to wait.
const httpTimeout = 3 * time.Second

// customHTTP is the client for URL rows. One client, reused: a new one per
// request leaks connections on a switch that keeps them alive.
var customHTTP = &http.Client{Timeout: httpTimeout}

// Render substitutes {field} placeholders in tmpl.
//
// escape decides how a value is written, and it is not cosmetic: in a URL every
// value has to be percent-encoded, or the first portable callsign — F4BPO/P —
// puts a path separator in the middle of a query string, and a name with a
// space breaks the request outright. In a UDP payload the opposite holds: the
// receiver wants the text as typed.
//
// A placeholder with no value becomes empty rather than staying as "{freq}".
// Leaving the braces would send them down the wire, and a receiver would store
// the literal text as if it meant something.
func Render(tmpl string, fields map[string]string, escape bool) string {
	var b strings.Builder
	for {
		i := strings.IndexByte(tmpl, '{')
		if i < 0 {
			b.WriteString(tmpl)
			return b.String()
		}
		j := strings.IndexByte(tmpl[i:], '}')
		if j < 0 {
			b.WriteString(tmpl) // an unclosed brace is literal text
			return b.String()
		}
		b.WriteString(tmpl[:i])
		key := strings.ToLower(strings.TrimSpace(tmpl[i+1 : i+j]))
		v := fields[key]
		if escape {
			v = url.QueryEscape(v)
		}
		b.WriteString(v)
		tmpl = tmpl[i+j+1:]
	}
}

// lineEnding turns the stored choice into the bytes appended to a UDP payload.
// Many receivers read a line at a time and wait for a terminator; a trailing
// space in a text box is invisible and lost on the first copy-paste, so this is
// a choice rather than something to type.
func lineEnding(s string) string {
	switch strings.ToLower(strings.TrimSpace(s)) {
	case "lf":
		return "\n"
	case "crlf":
		return "\r\n"
	}
	return ""
}

// EmitTrigger fires every enabled custom row bound to this trigger.
//
// Runs the sends in the background: an HTTP request must never sit on the path
// of the thing that caused it — a band change waiting on an antenna switch is a
// band change the operator watches not happen.
func (m *Manager) EmitTrigger(t Trigger, fields map[string]string) {
	var rows []Config
	for _, c := range m.Outbound(ServiceCustom) {
		if c.Trigger == t {
			rows = append(rows, c)
		}
	}
	if len(rows) == 0 {
		return
	}
	for _, c := range rows {
		go m.fireCustom(c, fields)
	}
}

func (m *Manager) fireCustom(c Config, fields map[string]string) {
	if c.Transport == TransportURL {
		target := Render(c.URL, fields, true)
		if strings.TrimSpace(target) == "" {
			m.noteEmptyRender(c, c.URL)
			return
		}
		m.getURL(c, target)
		return
	}
	payload := Render(c.Template, fields, false) + lineEnding(c.LineEnd)
	if strings.TrimSpace(payload) == "" {
		m.noteEmptyRender(c, c.Template)
		return
	}
	m.sendTo(c, []byte(payload))
}

// noteEmptyRender reports a template that produced nothing.
//
// Silence here was the worst of the lot: a row whose template is a single
// placeholder the trigger does not carry — {freq} on a band change — renders
// empty, sends nothing, and looked exactly like a row that had never fired. The
// operator has no way to tell a typo from a trigger that is not reaching them.
//
// Throttled with the same per-row timer as a failing URL: a template that is
// wrong stays wrong on every band change.
func (m *Manager) noteEmptyRender(c Config, tmpl string) {
	m.noteCustomProblem(c, fmt.Sprintf(
		"on %s the message came out empty — %q filled in nothing. Check the field names against the list under the box.",
		c.Trigger, tmpl))
}

// getURL performs the request and reports what came back.
//
// The status code is logged, not just transport errors. A switch answering 404
// because a firmware update moved its endpoint is a total failure that looks
// exactly like success from here — the datagram equivalent does not exist, and
// an operator would have no way to tell.
func (m *Manager) getURL(c Config, target string) {
	ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
	if err != nil {
		applog.Printf("udp: [%s] bad URL %q: %v", c.Name, redactURL(target), err)
		return
	}
	resp, err := customHTTP.Do(req)
	if err != nil {
		m.noteCustomProblem(c, fmt.Sprintf("%s failed: %v", redactURL(target), err))
		return
	}
	defer resp.Body.Close()
	if resp.StatusCode >= 400 {
		m.noteCustomProblem(c, fmt.Sprintf("%s answered %s", redactURL(target), resp.Status))
		return
	}
	applog.Printf("udp: [%s] on %s GET %s → %s", c.Name, c.Trigger, redactURL(target), resp.Status)
}

// redactURL hides the password in a URL before it reaches the log.
//
// The URL is what makes this feature debuggable, so it is logged in full — but
// an operator who put http://admin:secret@switch/ in the box is going to send
// that log to somebody when something goes wrong. Storing the password as typed
// was a deliberate choice for LAN gear; writing it into a file that travels is
// not the same choice, and was never made.
func redactURL(raw string) string {
	u, err := url.Parse(raw)
	if err != nil || u.User == nil {
		return raw
	}
	if _, hasPass := u.User.Password(); !hasPass {
		return raw
	}
	u.User = url.UserPassword(u.User.Username(), "***")
	// Qt-style escaping of the placeholder would give %2A%2A%2A; put it back so
	// the line stays readable.
	return strings.ReplaceAll(u.String(), "%2A%2A%2A", "***")
}

// noteCustomProblem logs a failing URL row, throttled per row: a switch that is
// off answers the same way on every band change, and this fires on every one.
func (m *Manager) noteCustomProblem(c Config, msg string) {
	m.httpFailMu.Lock()
	if m.httpFailAt == nil {
		m.httpFailAt = map[int64]time.Time{}
	}
	last := m.httpFailAt[c.ID]
	now := time.Now()
	quiet := now.Sub(last) < 5*time.Minute
	if !quiet {
		m.httpFailAt[c.ID] = now
	}
	m.httpFailMu.Unlock()
	if quiet {
		return
	}
	applog.Printf("udp: [%s] %s", c.Name, msg)
}
