package server

import (
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"vocat/internal/device"
)

func esimUnavailable(w http.ResponseWriter) {
	writeError(w, http.StatusNotImplemented, "esim_operation_unavailable", "This specific eSIM operation is not implemented.")
}

// handleESIM routes every /devices/{id}/esim* path.
func (s *Server) handleESIM(w http.ResponseWriter, r *http.Request, rest []string, physicalID string, physicalPresent bool) bool {
	if len(rest) == 0 || (len(rest) == 1 && strings.TrimSpace(rest[0]) == "") {
		if !requireMethod(w, r, http.MethodGet) {
			return true
		}
		s.writeEsimOverview(w, r, physicalID, physicalPresent)
		return true
	}

	switch rest[0] {
	case "profiles":
		if len(rest) == 1 {
			if !requireMethod(w, r, http.MethodGet) {
				return true
			}
			s.writeEsimGroups(w, r, physicalID, physicalPresent)
			return true
		}
		if len(rest) == 2 && r.Method == http.MethodDelete {
			s.handleEsimDelete(w, r, physicalID, physicalPresent, rest[1])
			return true
		}
		if len(rest) == 2 && r.Method == http.MethodPatch {
			s.handleEsimRename(w, r, physicalID, physicalPresent, rest[1])
			return true
		}
		esimUnavailable(w)
		return true
	case "notifications":
		if len(rest) == 1 {
			if !requireMethod(w, r, http.MethodGet) {
				return true
			}
			// No LPA download backend, so there are never pending notifications.
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": []any{}}})
			return true
		}
		// notifications/{id}/actions/retry
		esimUnavailable(w)
		return true
	case "actions":
		if len(rest) == 2 && rest[1] == "switch" {
			if !requireMethod(w, r, http.MethodPost) {
				return true
			}
			s.handleEsimSwitch(w, r, physicalID, physicalPresent)
			return true
		}
		if len(rest) == 2 && rest[1] == "disable" {
			if !requireMethod(w, r, http.MethodPost) {
				return true
			}
			s.handleEsimDisable(w, r, physicalID, physicalPresent)
			return true
		}
		if len(rest) == 2 && rest[1] == "download" {
			if !requireMethod(w, r, http.MethodGet) {
				return true
			}
			s.handleEsimDownload(w, r, physicalID, physicalPresent)
			return true
		}
		// Any other provisioning action is not implemented.
		esimUnavailable(w)
		return true
	default:
		return false
	}
}

// esimInfo loads the eUICC profile list. The string result is "ok" (use info),
// "empty" (no usable eUICC — render the empty state), or "error" (an error
// response has already been written).
func (s *Server) esimInfo(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool) (string, []device.EsimInventoryEntry) {
	if s.devices == nil || !physicalPresent {
		return "empty", nil
	}
	info, err := s.devices.ESIMInventory(r.Context(), physicalID)
	if err != nil {
		if errors.Is(err, device.ErrNoEUICC) {
			return "empty", nil
		}
		s.writeDeviceError(w, err)
		return "error", nil
	}
	return "ok", info
}

// writeEsimOverview returns { chipInfo, profiles } for the eSIM tab.
func (s *Server) writeEsimOverview(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool) {
	status, info := s.esimInfo(w, r, physicalID, physicalPresent)
	switch status {
	case "error":
		return
	case "empty":
		writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"chipInfo": nil, "profiles": []any{}}})
		return
	}
	chipInfo := esimInventoryChipInfo(info)
	groups := esimInventoryGroups(info)
	writeJSON(w, http.StatusOK, map[string]any{
		"data": map[string]any{
			"chipInfo": chipInfo,
			"profiles": groups,
		},
	})
}

func esimInventoryChipInfo(entries []device.EsimInventoryEntry) map[string]any {
	eids := make([]any, 0, len(entries))
	firmware := ""
	for _, entry := range entries {
		chip := entry.Chip
		eid := map[string]any{"eid": chip.EID, "aid": chip.AID}
		if chip.HasFreeNvram {
			eid["freeNvramBytes"] = chip.FreeNvramBytes
			eid["freeNvram"] = fmt.Sprintf("%.2f KB", float64(chip.FreeNvramBytes)/1024)
		}
		if chip.Manufacturer != "" {
			eid["manufacturer"] = chip.Manufacturer
		}
		if len(chip.Certificates) > 0 {
			eid["certificates"] = chip.Certificates
		}
		if len(chip.TrustedCIs) > 0 {
			eid["trustedCiKeyIds"] = chip.TrustedCIs
		}
		if chip.DefaultSmdpAddress != "" {
			eid["defaultSmdpAddress"] = chip.DefaultSmdpAddress
		}
		if chip.RootDsAddress != "" {
			eid["rootDsAddress"] = chip.RootDsAddress
		}
		if chip.SAS != "" {
			eid["sasAccreditationNumber"] = chip.SAS
		}
		eids = append(eids, eid)
		if firmware == "" {
			firmware = chip.FirmwareVer
		}
	}
	result := map[string]any{"eids": eids}
	if firmware != "" {
		result["firmware"] = firmware
	}
	return result
}

func esimInventoryGroups(entries []device.EsimInventoryEntry) []map[string]any {
	groups := make([]map[string]any, 0, len(entries))
	for _, entry := range entries {
		groups = append(groups, esimGroups(entry.Info)...)
	}
	return groups
}

// esimChipInfo reads the eUICC chip header (EID, firmware, free NVRAM,
// manufacturer, CI certificates, SM-DP+/Root SM-DS addresses, SAS, info source)
// for the eSIM tab. On any read failure it returns a sparse object so the
// profile list still renders.
func (s *Server) esimChipInfo(r *http.Request, physicalID string) map[string]any {
	chip, err := s.devices.ESIMChipInfo(r.Context(), physicalID)
	if err != nil || chip == nil {
		return map[string]any{}
	}
	eid := map[string]any{
		"eid": chip.EID,
		"aid": chip.AID,
	}
	if chip.HasFreeNvram {
		eid["freeNvramBytes"] = chip.FreeNvramBytes
		eid["freeNvram"] = fmt.Sprintf("%.2f KB", float64(chip.FreeNvramBytes)/1024)
	}
	if chip.Manufacturer != "" {
		eid["manufacturer"] = chip.Manufacturer
	}
	if len(chip.Certificates) > 0 {
		eid["certificates"] = chip.Certificates
	}
	if len(chip.TrustedCIs) > 0 {
		eid["trustedCiKeyIds"] = chip.TrustedCIs
	}
	if chip.DefaultSmdpAddress != "" {
		eid["defaultSmdpAddress"] = chip.DefaultSmdpAddress
	}
	if chip.RootDsAddress != "" {
		eid["rootDsAddress"] = chip.RootDsAddress
	}
	if chip.SAS != "" {
		eid["sasAccreditationNumber"] = chip.SAS
	}
	chipMap := map[string]any{
		"eids": []any{eid},
	}
	if chip.FirmwareVer != "" {
		chipMap["firmware"] = chip.FirmwareVer
	}
	return chipMap
}

// writeEsimGroups returns just the profile groups for the /esim/profiles call.
func (s *Server) writeEsimGroups(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool) {
	status, info := s.esimInfo(w, r, physicalID, physicalPresent)
	switch status {
	case "error":
		return
	case "empty":
		writeJSON(w, http.StatusOK, map[string]any{"data": []any{}})
		return
	}
	groups := esimInventoryGroups(info)
	writeJSON(w, http.StatusOK, map[string]any{"data": groups})
}

// GetProfilesInfo does not include an EID on every eUICC implementation. The
// EC20 hosts one physical eUICC, so associate the separately-read chip identity
// with that sole profile group. Without this, the SPA cannot match the group to
// its manufacturer/certificate/production metadata even though it was read.
func attachSingleEUICCIdentity(groups []map[string]any, chipInfo map[string]any) {
	if len(groups) != 1 {
		return
	}
	eids, ok := chipInfo["eids"].([]any)
	if !ok || len(eids) != 1 {
		return
	}
	identity, ok := eids[0].(map[string]any)
	if !ok {
		return
	}
	groupEID, _ := groups[0]["eid"].(string)
	chipEID, _ := identity["eid"].(string)
	if strings.TrimSpace(groupEID) == "" && strings.TrimSpace(chipEID) != "" {
		groups[0]["eid"] = strings.TrimSpace(chipEID)
	}
	groupAID, _ := groups[0]["aidHex"].(string)
	chipAID, _ := identity["aid"].(string)
	if strings.TrimSpace(groupAID) == "" && strings.TrimSpace(chipAID) != "" {
		groups[0]["aidHex"] = strings.TrimSpace(chipAID)
	}
}

// esimGroups flattens the eUICC profile list into the SPA's per-eUICC groups
// (the EC20 hosts a single eUICC, so this is normally one group).
func esimGroups(info device.EsimInfo) []map[string]any {
	profiles := make([]map[string]any, 0, len(info.Profiles))
	for _, p := range info.Profiles {
		profiles = append(profiles, map[string]any{
			"iccid":               p.ICCID,
			"name":                firstNonEmpty(p.Nickname, p.Name),
			"serviceProviderName": p.ServiceProvider,
			"state":               p.State,
			"stateText":           p.StateText,
			"classText":           p.Class,
		})
	}
	return []map[string]any{
		{
			"eid":      info.EID,
			"aidHex":   info.AID,
			"profiles": profiles,
		},
	}
}

func (s *Server) handleEsimRename(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool, iccid string) {
	if s.devices == nil {
		writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
		return
	}
	if !physicalPresent {
		writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
		return
	}
	iccid = strings.TrimSpace(iccid)
	if iccid == "" {
		writeError(w, http.StatusBadRequest, "invalid_request", "iccid is required")
		return
	}
	var request struct {
		Name   string `json:"name"`
		AIDHex string `json:"aid_hex"` // accepted for the multi-eUICC SPA contract; ICCID addresses the profile
	}
	if err := s.decodeJSON(w, r, &request); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
		return
	}
	nickname := strings.TrimSpace(request.Name)
	if nickname == "" {
		writeError(w, http.StatusBadRequest, "invalid_request", "profile nickname is required")
		return
	}
	if err := s.devices.ESIMRenameProfile(r.Context(), physicalID, iccid, nickname, request.AIDHex); err != nil {
		s.writeDeviceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"status": "renamed", "iccid": iccid, "name": nickname}})
}

// handleEsimSwitch enables one already-installed profile by ICCID (切卡). The
// eUICC EnableProfile command needs no authentication key.
func (s *Server) handleEsimSwitch(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool) {
	if s.devices == nil {
		writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
		return
	}
	if !physicalPresent {
		writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
		return
	}
	var request struct {
		ICCID  string `json:"iccid"`
		AIDHex string `json:"aid_hex"` // accepted for contract compatibility; switching keys off iccid
	}
	if err := s.decodeJSON(w, r, &request); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
		return
	}
	iccid := strings.TrimSpace(request.ICCID)
	if iccid == "" {
		writeError(w, http.StatusBadRequest, "invalid_request", "iccid is required")
		return
	}
	// A confirmed profile switch includes the EC20 reset and a live ICCID read,
	// which normally takes longer than the server's ordinary response deadline.
	controller := http.NewResponseController(w)
	_ = controller.SetWriteDeadline(time.Time{})
	if err := s.devices.ESIMSwitchProfile(r.Context(), physicalID, iccid, request.AIDHex); err != nil {
		s.writeDeviceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"status": "switched", "iccid": iccid, "verified": true}})
}

func (s *Server) handleEsimDisable(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool) {
	if s.devices == nil {
		writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
		return
	}
	if !physicalPresent {
		writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
		return
	}
	var request struct {
		ICCID  string `json:"iccid"`
		AIDHex string `json:"aid_hex"` // accepted for the multi-eUICC SPA contract; disabling keys off ICCID
	}
	if err := s.decodeJSON(w, r, &request); err != nil {
		writeError(w, http.StatusBadRequest, "invalid_request", err.Error())
		return
	}
	iccid := strings.TrimSpace(request.ICCID)
	if iccid == "" {
		writeError(w, http.StatusBadRequest, "invalid_request", "iccid is required")
		return
	}
	if err := s.devices.ESIMDisableProfile(r.Context(), physicalID, iccid, request.AIDHex); err != nil {
		s.writeDeviceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"status": "disabled", "iccid": iccid, "recovering": true}})
}

func (s *Server) handleEsimDelete(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool, iccid string) {
	if s.devices == nil {
		writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
		return
	}
	if !physicalPresent {
		writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
		return
	}
	iccid = strings.TrimSpace(iccid)
	if iccid == "" {
		writeError(w, http.StatusBadRequest, "invalid_request", "iccid is required")
		return
	}
	result, err := s.devices.ESIMDeleteProfile(r.Context(), physicalID, iccid, r.URL.Query().Get("aid_hex"))
	if err != nil {
		s.writeDeviceError(w, err)
		return
	}
	data := map[string]any{
		"status":     "deleted",
		"iccid":      iccid,
		"spaceDelta": map[string]any{"direction": "reclaimed", "bytes": result.SpaceDelta},
	}
	if result.Warning != "" {
		data["warning"] = result.Warning
	}
	writeJSON(w, http.StatusOK, map[string]any{"data": data})
}

// handleEsimDownload streams one eSIM profile download (写卡) as Server-Sent
// Events. The SPA drives it with GET + query params (smdp/matching_id/
// confirmation_code/aid_hex/imei) and reads `data: {step,msg,pct,...}` lines.
// The event field names (step/msg/pct/code/space_delta/warning) match the
// reference contract byte-for-byte, so the frontend needs no changes.
func (s *Server) handleEsimDownload(w http.ResponseWriter, r *http.Request, physicalID string, physicalPresent bool) {
	if s.devices == nil {
		writeError(w, http.StatusServiceUnavailable, "device_manager_unavailable", "device manager is unavailable")
		return
	}
	if !physicalPresent {
		writeError(w, http.StatusServiceUnavailable, "physical_device_missing", "the configured modem is not present on this Linux host")
		return
	}
	query := r.URL.Query()
	params := device.EsimDownloadParams{
		SMDP:             query.Get("smdp"),
		MatchingID:       query.Get("matching_id"),
		ConfirmationCode: query.Get("confirmation_code"),
		AIDHex:           query.Get("aid_hex"),
		IMEI:             query.Get("imei"),
	}
	if strings.TrimSpace(params.SMDP) == "" {
		writeError(w, http.StatusBadRequest, "invalid_request", "smdp 为必填项")
		return
	}

	controller := beginSSE(w)
	emit := func(payload map[string]any) {
		// A failed write means the client went away; r.Context() is then already
		// cancelled, so the device layer stops the download on its own.
		_ = writeSSEEvent(w, controller, "progress", payload)
	}

	result, err := s.devices.ESIMDownloadProfile(r.Context(), physicalID, params, func(p device.EsimProgress) {
		emit(map[string]any{"step": p.Step, "msg": p.Msg, "pct": p.Pct})
	})
	if err != nil {
		emit(map[string]any{
			"step": "error",
			"msg":  "下载失败: " + err.Error(),
			"pct":  -1,
			"code": device.ESIMDownloadErrorCode(err),
		})
		return
	}
	done := map[string]any{
		"step":        "done",
		"msg":         "Profile 下载完成",
		"pct":         100,
		"space_delta": map[string]any{"direction": "consumed", "bytes": result.SpaceDelta},
	}
	if result.Warning != "" {
		done["warning"] = result.Warning
	}
	emit(done)
}
