package handlers

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
	"github.com/gesellix/bose-soundtouch/pkg/service/health"
	"github.com/gesellix/bose-soundtouch/pkg/service/setup"
	"github.com/go-chi/chi/v5"
)

// accountIDSuggestionsResponse is the body of GET /setup/account-id-suggestions/{deviceId}.
// `current` is the device's existing margeAccountUUID (empty when the device is fresh / factory-reset).
// `known` is the list of accountIDs already present in the local datastore, so the UI can offer
// the user a way to re-attach a fresh device to an existing account.
type accountIDSuggestionsResponse struct {
	Current string   `json:"current"`
	Known   []string `json:"known"`
}

// HandleAccountIDSuggestions returns the device's current account ID (from
// :8090/info, empty if unset) plus the list of account IDs already present
// in the local datastore.
func (s *Server) HandleAccountIDSuggestions(w http.ResponseWriter, r *http.Request) {
	deviceID := chi.URLParam(r, "deviceId")
	if deviceID == "" {
		writeJSONError(w, http.StatusBadRequest, "Device ID is required")
		return
	}

	deviceIP, err := s.resolveDeviceIDToIP(deviceID)
	if err != nil {
		writeJSONError(w, http.StatusNotFound, err.Error())
		return
	}

	resp := accountIDSuggestionsResponse{}

	if info, err := s.sm.GetLiveDeviceInfo(deviceIP); err == nil {
		resp.Current = info.MargeAccountUUID
	}

	if known, err := s.ds.ListAccounts(); err == nil {
		resp.Known = known
	}

	w.Header().Set("Content-Type", "application/json")

	if err := json.NewEncoder(w).Encode(resp); err != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
	}
}

// pairAccountResponse is the body of POST /setup/pair-account/{deviceId}.
type pairAccountResponse struct {
	OK     bool                    `json:"ok"`
	Result setup.PairAccountResult `json:"result"`
	Output string                  `json:"output"`
	Error  string                  `json:"error,omitempty"`
}

// HandlePairAccount associates the device with the supplied account ID,
// trying HTTP /setMargeAccount first and falling back to telnet
// `envswitch accountid set`.
//
// Query params:
//   - account_id (required) — must pass datastore.IsSafeIdentifier
func (s *Server) HandlePairAccount(w http.ResponseWriter, r *http.Request) {
	deviceID := chi.URLParam(r, "deviceId")
	if deviceID == "" {
		writeJSONError(w, http.StatusBadRequest, "Device ID is required")
		return
	}

	accountID := r.URL.Query().Get("account_id")
	if !datastore.IsSafeIdentifier(accountID) {
		writeJSONError(w, http.StatusBadRequest, "account_id must be a non-empty, path-safe identifier")
		return
	}

	deviceIP, err := s.resolveDeviceIDToIP(deviceID)
	if err != nil {
		writeJSONError(w, http.StatusNotFound, err.Error())
		return
	}

	var t setup.TelnetClient

	if s.sm.NewTelnet != nil {
		t = s.sm.NewTelnet(deviceIP)
		if dialErr := t.Dial(); dialErr != nil {
			// Telnet not reachable — fall through with t=nil so PairAccount
			// can decide based on HTTP availability alone.
			t = nil
		} else {
			defer func() { _ = t.Close() }()
		}
	}

	result, output, err := s.sm.PairAccount(deviceIP, accountID, t)

	w.Header().Set("Content-Type", "application/json")

	body := pairAccountResponse{
		OK:     err == nil,
		Result: result,
		Output: output,
	}

	if err != nil {
		body.Error = err.Error()

		w.WriteHeader(http.StatusInternalServerError)
	}

	if encErr := json.NewEncoder(w).Encode(body); encErr != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
	}
}

// completeSpeakerPairingFix is the FixFunc registered for
// (CheckIDSpeakerInfoReachable, FixIDCompleteSpeakerPairing). It
// completes pairing on a speaker whose /info reports an empty
// <margeAccountUUID> by:
//
//  1. Looking up the device's current IP via ListAllDevices.
//  2. Choosing the pair-with account: target.Account when the
//     detection-side suggestion populated it, else generating a fresh
//     7-digit ID with setup.GenerateAccountID.
//  3. Dispatching through setup.Manager.PairAccount, which tries
//     /setMargeAccount over HTTP first and falls back to telnet.
//
// Returns a user-facing success message that names the chosen account
// and the method that succeeded; the framework forwards it to the UI.
func (s *Server) completeSpeakerPairingFix(target health.Target) (string, error) {
	if target.Device == "" {
		return "", fmt.Errorf("device is required")
	}

	deviceIP, err := s.resolveDeviceIDToIP(target.Device)
	if err != nil {
		return "", fmt.Errorf("locate device %s: %w", target.Device, err)
	}

	accountID := target.Account
	if !datastore.IsSafeIdentifier(accountID) {
		known, _ := s.ds.ListAccounts()

		generated, genErr := setup.GenerateAccountID(known)
		if genErr != nil {
			return "", fmt.Errorf("generate account ID: %w", genErr)
		}

		accountID = generated
	}

	var t setup.TelnetClient
	if s.sm.NewTelnet != nil {
		t = s.sm.NewTelnet(deviceIP)
		if dialErr := t.Dial(); dialErr != nil {
			t = nil
		} else {
			defer func() { _ = t.Close() }()
		}
	}

	result, output, err := s.sm.PairAccount(deviceIP, accountID, t)
	if err != nil {
		return "", fmt.Errorf("pair speaker %s with account %s: %w (path output: %s)", target.Device, accountID, err, strings.TrimSpace(output))
	}

	method := result.Method
	if method == "" {
		method = "unknown"
	}

	return fmt.Sprintf("Paired speaker %s with account %s via %s. The speaker will re-fetch /full on its own; playback selection should start working within seconds.",
		target.Device, accountID, method), nil
}

// jsonErrorBody is the static shape of error responses from this file.
// Avoiding map[string]interface{} keeps errchkjson satisfied: the typed
// struct guarantees encoding can't fail with a runtime type error.
type jsonErrorBody struct {
	OK      bool   `json:"ok"`
	Message string `json:"message"`
}

// writeJSONError is a small helper for the handlers in this file to keep
// error wiring out of the happy path. It mirrors what the rest of the
// package does inline.
func writeJSONError(w http.ResponseWriter, status int, message string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)

	if err := json.NewEncoder(w).Encode(jsonErrorBody{OK: false, Message: message}); err != nil {
		http.Error(w, "Failed to encode response", http.StatusInternalServerError)
	}
}
