package controller

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	piaprotocol "github.com/mhsanaei/3x-ui/v3/internal/pia"
	"github.com/mhsanaei/3x-ui/v3/internal/util/common"
	"github.com/mhsanaei/3x-ui/v3/internal/web/service"
	"github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
	"github.com/mhsanaei/3x-ui/v3/internal/web/service/outbound"
	"github.com/mhsanaei/3x-ui/v3/internal/xray"

	"github.com/gin-gonic/gin"
)

// XraySettingController handles Xray configuration and settings operations.
type XraySettingController struct {
	XraySettingService          service.XraySettingService
	SettingService              service.SettingService
	InboundService              service.InboundService
	OutboundService             outbound.OutboundService
	XrayService                 service.XrayService
	WarpService                 integration.WarpService
	NordService                 integration.NordService
	PiaService                  integration.PiaService
	OutboundSubscriptionService service.OutboundSubscriptionService
	GeodataService              service.GeodataService
}

// NewXraySettingController creates a new XraySettingController and initializes its routes.
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
	a := &XraySettingController{PiaService: *integration.NewPiaService()}
	a.initRouter(g)
	return a
}

// initRouter sets up the routes for Xray settings management.
func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
	g = g.Group("/xray")
	g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
	g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
	g.GET("/getXrayResult", a.getXrayResult)

	g.POST("/", a.getXraySetting)
	g.POST("/warp/:action", a.warp)
	g.POST("/nord/:action", a.nord)
	g.POST("/pia/:action", a.pia)
	g.POST("/update", a.updateSetting)
	g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
	g.POST("/testOutbound", a.testOutbound)
	g.POST("/testOutbounds", a.testOutbounds)
	g.POST("/balancerStatus", a.balancerStatus)
	g.POST("/balancerOverride", a.balancerOverride)
	g.POST("/routeTest", a.routeTest)

	g.GET("/geodata/files", a.geodataFiles)
	g.GET("/geodata/categories", a.geodataCategories)
	g.GET("/geodata/entries", a.geodataEntries)
	g.POST("/geodata/validate", a.geodataValidate)

	// Outbound subscription (remote outbound lists)
	g.GET("/outbound-subs", a.listOutboundSubs)
	g.POST("/outbound-subs", a.createOutboundSub)
	g.POST("/outbound-subs/:id/refresh", a.refreshOutboundSub)
	g.POST("/outbound-subs/:id/move", a.moveOutboundSub)
	g.POST("/outbound-subs/:id", a.updateOutboundSub)
	g.DELETE("/outbound-subs/:id", a.deleteOutboundSub)
	g.POST("/outbound-subs/:id/del", a.deleteOutboundSub) // POST alias for clients that can't send DELETE
	g.POST("/outbound-subs/parse", a.parseOutboundSubURL) // preview without saving
}

// getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
func (a *XraySettingController) getXraySetting(c *gin.Context) {
	xraySetting, err := a.SettingService.GetXrayConfigTemplate()
	if err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
		return
	}
	// Older versions of this handler embedded the raw DB value as
	// `xraySetting` in the response without checking if the value
	// already had that wrapper shape. When the frontend saved it
	// back through the textarea verbatim, the wrapper got persisted
	// and every subsequent save nested another layer, which is what
	// eventually produced the blank Xray Settings page in #4059.
	// Strip any such wrapper here, and heal the DB if we found one so
	// the next read is O(1) instead of climbing the same pile again.
	if unwrapped := service.UnwrapXrayTemplateConfig(xraySetting); unwrapped != xraySetting {
		if saveErr := a.XraySettingService.SaveXraySetting(unwrapped); saveErr == nil {
			xraySetting = unwrapped
		} else {
			// Don't fail the read — just serve the unwrapped value
			// and leave the DB healing for a later save.
			xraySetting = unwrapped
		}
	}
	inboundTags, err := a.InboundService.GetInboundTags()
	if err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
		return
	}
	clientReverseTags, err := a.InboundService.GetClientReverseTags()
	if err != nil {
		clientReverseTags = "[]"
	}
	outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
	if outboundTestUrl == "" {
		outboundTestUrl = "https://www.google.com/generate_204"
	}
	xrayResponse := map[string]any{
		"xraySetting":       json.RawMessage(xraySetting),
		"inboundTags":       json.RawMessage(inboundTags),
		"clientReverseTags": json.RawMessage(clientReverseTags),
		"outboundTestUrl":   outboundTestUrl,
	}

	// Surface subscription outbounds (and their tags) so the frontend can:
	// - show them as read-only items in the Outbounds tab
	// - let users pick them in balancers and routing rules
	// These are not part of the editable template; they are injected at runtime.
	if subObs, err := a.OutboundSubscriptionService.AllActiveOutbounds(); err == nil && len(subObs) > 0 {
		xrayResponse["subscriptionOutbounds"] = subObs
	}
	if subTags, err := a.OutboundSubscriptionService.AllActiveOutboundTags(); err == nil && len(subTags) > 0 {
		xrayResponse["subscriptionOutboundTags"] = subTags
	}
	result, err := json.Marshal(xrayResponse)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
		return
	}
	jsonObj(c, string(result), nil)
}

// updateSetting updates the Xray configuration settings and applies them to
// the running core right away — through the gRPC API when only inbounds,
// outbounds or routing rules changed, with a process restart otherwise.
func (a *XraySettingController) updateSetting(c *gin.Context) {
	xraySetting := c.PostForm("xraySetting")
	if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
		return
	}
	outboundTestUrl := c.PostForm("outboundTestUrl")
	if outboundTestUrl == "" {
		outboundTestUrl = "https://www.google.com/generate_204"
	}
	if err := a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl); err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
		return
	}
	// Only reconcile a running core; a manually stopped xray stays stopped.
	if a.XrayService.IsXrayRunning() {
		if err := a.XrayService.RestartXray(false); err != nil {
			jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
			return
		}
	}
	jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
}

// getDefaultXrayConfig retrieves the default Xray configuration.
func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
	defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
	if err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
		return
	}
	jsonObj(c, defaultJsonConfig, nil)
}

// getXrayResult retrieves the current Xray service result.
func (a *XraySettingController) getXrayResult(c *gin.Context) {
	jsonObj(c, a.XrayService.GetXrayResult(), nil)
}

// warp handles Warp-related operations based on the action parameter.
func (a *XraySettingController) warp(c *gin.Context) {
	action := c.Param("action")
	var resp string
	var err error
	switch action {
	case "data":
		resp, err = a.WarpService.GetWarpData()
	case "del":
		err = a.WarpService.DelWarpData()
	case "config":
		resp, err = a.WarpService.GetWarpConfig()
	case "reg":
		skey := c.PostForm("privateKey")
		pkey := c.PostForm("publicKey")
		resp, err = a.WarpService.RegWarp(skey, pkey)
	case "changeIp":
		resp, err = a.WarpService.ChangeWarpIP()
		if err == nil {
			a.XrayService.SetToNeedRestart()
			// Restart the auto-update clock so a scheduled rotation
			// doesn't fire right after this manual one.
			err = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
		}
	case "license":
		license := c.PostForm("license")
		resp, err = a.WarpService.SetWarpLicense(license)
	case "interval":
		interval, convErr := strconv.Atoi(c.PostForm("interval"))
		if convErr != nil || interval < 0 {
			err = common.NewError("invalid warp update interval")
		} else if err = a.SettingService.SetWarpUpdateInterval(interval); err == nil && interval > 0 {
			// Count the interval from now rather than from epoch 0,
			// otherwise the job would rotate on its next tick.
			err = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
		}
	}

	jsonObj(c, resp, err)
}

// nord handles NordVPN-related operations based on the action parameter.
func (a *XraySettingController) nord(c *gin.Context) {
	action := c.Param("action")
	var resp string
	var err error
	switch action {
	case "countries":
		resp, err = a.NordService.GetCountries()
	case "servers":
		countryId := c.PostForm("countryId")
		resp, err = a.NordService.GetServers(countryId)
	case "reg":
		token := c.PostForm("token")
		resp, err = a.NordService.GetCredentials(token)
	case "setKey":
		key := c.PostForm("key")
		resp, err = a.NordService.SetKey(key)
	case "data":
		resp, err = a.NordService.GetNordData()
	case "del":
		err = a.NordService.DelNordData()
	}

	jsonObj(c, resp, err)
}

func (a *XraySettingController) pia(c *gin.Context) {
	action := c.Param("action")
	var resp any
	var err error
	switch action {
	case "countries":
		resp, err = a.PiaService.GetCountries()
	case "servers":
		resp, err = a.PiaService.GetServers(c.PostForm("countryCode"))
	case "reg":
		resp, err = a.PiaService.Login(c.PostForm("username"), c.PostForm("password"))
	case "data":
		resp, err = a.PiaService.GetPiaData()
	case "del":
		err = a.PiaService.DelPiaData()
	case "addKey":
		resp, err = a.PiaService.AddKey(c.PostForm("hostname"))
	default:
		jsonMsg(c, "unknown action", common.NewError("unknown action"))
		return
	}
	if err != nil {
		var pe *piaprotocol.Error
		if errors.As(err, &pe) && pe != nil {
			jsonObj(c, nil, common.NewError(pe.Message))
			return
		}
		jsonObj(c, nil, err)
		return
	}
	jsonObj(c, resp, nil)
}

// getOutboundsTraffic retrieves the traffic statistics for outbounds.
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
	outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
	if err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getOutboundTrafficError"), err)
		return
	}
	jsonObj(c, outboundsTraffic, nil)
}

// resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
	tag := c.PostForm("tag")
	err := a.OutboundService.ResetOutboundTraffic(tag)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.resetOutboundTrafficError"), err)
		return
	}
	jsonObj(c, "", nil)
}

// testOutbound tests an outbound configuration and returns the delay/response time.
// Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
// Optional form "mode": "tcp" for a fast dial-only probe, "real" for the cold
// full-request delay, anything else (default) for a full HTTP probe through a temp xray instance.
func (a *XraySettingController) testOutbound(c *gin.Context) {
	outboundJSON := c.PostForm("outbound")
	allOutboundsJSON := c.PostForm("allOutbounds")
	mode := c.PostForm("mode")

	if outboundJSON == "" {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbound parameter is required"))
		return
	}

	// Load the test URL from server settings to prevent SSRF via user-controlled URLs
	testURL, _ := a.SettingService.GetXrayOutboundTestUrl()
	testURL, err := service.SanitizePublicHTTPURL(testURL, false)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}

	result, err := a.OutboundService.TestOutbound(outboundJSON, testURL, allOutboundsJSON, mode)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}

	jsonObj(c, result, nil)
}

// testOutbounds tests a batch of outbound configurations through one shared
// temp xray instance and returns an array of results in input order.
// Form "outbounds": JSON array of outbound configs (required).
// Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
// Optional form "mode": "tcp" for fast dial-only probes, "real" for the cold
// full-request delay, anything else (default) for real HTTP requests routed through each outbound.
func (a *XraySettingController) testOutbounds(c *gin.Context) {
	outboundsJSON := c.PostForm("outbounds")
	allOutboundsJSON := c.PostForm("allOutbounds")
	mode := c.PostForm("mode")

	if outboundsJSON == "" {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbounds parameter is required"))
		return
	}

	// Load the test URL from server settings to prevent SSRF via user-controlled URLs
	testURL, _ := a.SettingService.GetXrayOutboundTestUrl()
	testURL, err := service.SanitizePublicHTTPURL(testURL, false)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}

	results, err := a.OutboundService.TestOutbounds(outboundsJSON, testURL, allOutboundsJSON, mode)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}

	jsonObj(c, results, nil)
}

// balancerStatus reports the live state (override + strategy picks) of the
// balancer tags given as a comma-separated "tags" form field.
func (a *XraySettingController) balancerStatus(c *gin.Context) {
	raw := c.PostForm("tags")
	var tags []string
	for tag := range strings.SplitSeq(raw, ",") {
		if tag = strings.TrimSpace(tag); tag != "" {
			tags = append(tags, tag)
		}
	}
	statuses, err := a.XrayService.GetBalancersStatus(tags)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}
	byTag := make(map[string]service.BalancerStatus, len(statuses))
	for _, status := range statuses {
		byTag[status.Tag] = status
	}
	jsonObj(c, byTag, nil)
}

// balancerOverride forces a balancer to a specific outbound tag; an empty
// "target" clears the override.
func (a *XraySettingController) balancerOverride(c *gin.Context) {
	tag := c.PostForm("tag")
	if tag == "" {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("tag is required"))
		return
	}
	target := c.PostForm("target")
	if err := a.XrayService.OverrideBalancer(tag, target); err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}
	jsonObj(c, "", nil)
}

// routeTest asks the running core which outbound it would route a synthetic
// connection to.
func (a *XraySettingController) routeTest(c *gin.Context) {
	port := 0
	if portStr := c.PostForm("port"); portStr != "" {
		parsed, err := strconv.Atoi(portStr)
		if err != nil || parsed < 0 || parsed > 65535 {
			jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("invalid port"))
			return
		}
		port = parsed
	}
	req := xray.RouteTestRequest{
		InboundTag: c.PostForm("inboundTag"),
		Domain:     c.PostForm("domain"),
		IP:         c.PostForm("ip"),
		Port:       port,
		Network:    c.PostForm("network"),
		Protocol:   c.PostForm("protocol"),
		Email:      c.PostForm("email"),
	}
	if req.Domain == "" && req.IP == "" {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("domain or ip is required"))
		return
	}
	result, err := a.XrayService.TestRoute(req)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}
	jsonObj(c, result, nil)
}

// maxGeodataTokens bounds one validation request; a routing rule listing more
// categories than this is not something the panel needs to answer for.
const maxGeodataTokens = 500

// geodataFiles lists the geo databases Xray resolves geosite:/geoip: tokens
// against, including ones that failed to parse.
func (a *XraySettingController) geodataFiles(c *gin.Context) {
	files, err := a.GeodataService.Files()
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}
	jsonObj(c, files, nil)
}

// geodataCategories returns one page of a database's categories.
func (a *XraySettingController) geodataCategories(c *gin.Context) {
	offset, limit := geodataPaging(c)
	page, err := a.GeodataService.Categories(c.Query("file"), c.Query("q"), offset, limit)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}
	jsonObj(c, page, nil)
}

// geodataEntries returns one page of the domains or CIDRs inside a category.
func (a *XraySettingController) geodataEntries(c *gin.Context) {
	code := c.Query("code")
	if code == "" {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("code is required"))
		return
	}
	offset, limit := geodataPaging(c)
	page, err := a.GeodataService.Entries(c.Query("file"), code, c.Query("q"), offset, limit)
	if err != nil {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
		return
	}
	jsonObj(c, page, nil)
}

// geodataValidate reports which routing tokens do not resolve against the
// databases on disk.
func (a *XraySettingController) geodataValidate(c *gin.Context) {
	// Split with a bound rather than splitting first: a 10 MB body of commas
	// would otherwise allocate millions of strings before the limit is checked.
	tokens := strings.SplitN(c.PostForm("tokens"), ",", maxGeodataTokens+1)
	if len(tokens) > maxGeodataTokens {
		jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewErrorf("too many tokens: over %d", maxGeodataTokens))
		return
	}
	jsonObj(c, a.GeodataService.Validate(c.PostForm("kind") == "ip", tokens), nil)
}

func geodataPaging(c *gin.Context) (int, int) {
	offset, err := strconv.Atoi(c.Query("offset"))
	if err != nil {
		offset = 0
	}
	limit, err := strconv.Atoi(c.Query("limit"))
	if err != nil {
		limit = 0
	}
	return offset, limit
}

// --- Outbound Subscription handlers ---

func (a *XraySettingController) listOutboundSubs(c *gin.Context) {
	list, err := a.OutboundSubscriptionService.List()
	if err != nil {
		jsonMsg(c, "Failed to list outbound subscriptions", err)
		return
	}
	jsonObj(c, list, nil)
}

func (a *XraySettingController) createOutboundSub(c *gin.Context) {
	remark := c.PostForm("remark")
	rawURL := c.PostForm("url")
	prefix := c.PostForm("tagPrefix")
	enabled := c.PostForm("enabled") != "false"
	allowPrivate := c.PostForm("allowPrivate") == "true"
	allowInsecure := c.PostForm("allowInsecure") == "true"
	prepend := c.PostForm("prepend") == "true"
	intervalStr := c.PostForm("updateInterval")
	interval := 600
	if intervalStr != "" {
		if v, err := parseIntSafe(intervalStr); err == nil && v > 0 {
			interval = v
		}
	}
	sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure)
	if err != nil {
		jsonMsg(c, "Failed to create outbound subscription", err)
		return
	}
	jsonObj(c, sub, nil)
}

func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
	id := c.Param("id")
	var subID int
	if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
		jsonMsg(c, "Invalid id", err)
		return
	}
	remark := c.PostForm("remark")
	rawURL := c.PostForm("url")
	prefix := c.PostForm("tagPrefix")
	enabled := c.PostForm("enabled") != "false"
	allowPrivate := c.PostForm("allowPrivate") == "true"
	allowInsecure := c.PostForm("allowInsecure") == "true"
	prepend := c.PostForm("prepend") == "true"
	intervalStr := c.PostForm("updateInterval")
	interval := 600
	if intervalStr != "" {
		if v, err := parseIntSafe(intervalStr); err == nil && v > 0 {
			interval = v
		}
	}
	if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure); err != nil {
		jsonMsg(c, "Failed to update outbound subscription", err)
		return
	}
	jsonObj(c, "", nil)
}

func (a *XraySettingController) deleteOutboundSub(c *gin.Context) {
	id := c.Param("id")
	var subID int
	if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
		jsonMsg(c, "Invalid id", err)
		return
	}
	if err := a.OutboundSubscriptionService.Delete(subID); err != nil {
		jsonMsg(c, "Failed to delete outbound subscription", err)
		return
	}
	// Signal that xray should drop this subscription's outbounds on next reload.
	a.XrayService.SetToNeedRestart()
	jsonObj(c, "", nil)
}

func (a *XraySettingController) refreshOutboundSub(c *gin.Context) {
	id := c.Param("id")
	var subID int
	if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
		jsonMsg(c, "Invalid id", err)
		return
	}
	obs, err := a.OutboundSubscriptionService.Refresh(subID)
	if err != nil {
		jsonMsg(c, "Refresh failed", err)
		return
	}
	// Signal that xray should pick up the new outbounds on next restart/reload
	a.XrayService.SetToNeedRestart()
	jsonObj(c, obs, nil)
}

func (a *XraySettingController) moveOutboundSub(c *gin.Context) {
	id := c.Param("id")
	var subID int
	if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
		jsonMsg(c, "Invalid id", err)
		return
	}
	up := c.PostForm("dir") == "up"
	if err := a.OutboundSubscriptionService.Move(subID, up); err != nil {
		jsonMsg(c, "Failed to reorder outbound subscription", err)
		return
	}
	// Order affects the merged outbounds, so xray needs a reload.
	a.XrayService.SetToNeedRestart()
	jsonObj(c, "", nil)
}

// parseOutboundSubURL is a preview endpoint: it fetches + parses the provided
// URL but does not persist anything. Useful for the "add subscription" flow
// so the user can see the resulting outbounds (and assigned tags) before saving.
func (a *XraySettingController) parseOutboundSubURL(c *gin.Context) {
	rawURL := c.PostForm("url")
	if rawURL == "" {
		jsonMsg(c, "url is required", common.NewError("missing url"))
		return
	}
	allowPrivate := c.PostForm("allowPrivate") == "true"
	allowInsecure := c.PostForm("allowInsecure") == "true"
	// Use a throw-away service instance; it only needs the settingService for proxy.
	svc := service.OutboundSubscriptionService{}
	// We don't have a direct "fetch once" that returns without storing, so we
	// temporarily create a disabled row, refresh it, then delete. Cleaner would
	// be to expose a pure ParseURL on the service, but this keeps the surface small.
	tmp, err := svc.Create("preview", rawURL, "", false, 600, allowPrivate, false, allowInsecure)
	if err != nil {
		jsonMsg(c, "Failed to preview subscription", err)
		return
	}
	obs, err := svc.Refresh(tmp.Id)
	// best-effort cleanup
	_ = svc.Delete(tmp.Id)
	if err != nil {
		jsonMsg(c, "Failed to fetch/parse subscription", err)
		return
	}
	jsonObj(c, obs, nil)
}

func parseIntSafe(s string) (int, error) {
	var v int
	_, err := fmt.Sscanf(s, "%d", &v)
	return v, err
}
