package handler

import (
	"context"
	"errors"
	"net/http"
	"os"
	"slices"
	"strconv"
	"strings"
	"time"

	coremodel "security-center-backend/internal/model"
	caasmmodel "security-center-backend/internal/modules/caasm/model"
	"security-center-backend/internal/modules/caasm/repository"
	"security-center-backend/internal/modules/caasm/service"
	"security-center-backend/internal/pkg/cache"
	"security-center-backend/internal/pkg/httpcache"
	"security-center-backend/internal/pkg/response"
	auditservice "security-center-backend/internal/service/audit"
	jwtpkg "security-center-backend/internal/service/jwt"

	"github.com/gin-gonic/gin"
	"github.com/goccy/go-json"
	"gorm.io/gorm"
)

type Handler struct {
	assets    *service.AssetService
	ports     *service.PortService
	domains   *service.DomainService
	records   *service.DNSRecordService
	accounts  *service.CloudAccountService
	jobs      *service.SyncJobService
	webhooks  *WebhookHandler
	config    *service.ConfigService
	apiRoutes *service.APIRouteAssetService
	runtime   *service.RuntimeControl
	audit     *auditservice.Service
	cache     *cache.Cache
}

func New(
	assets *service.AssetService,
	ports *service.PortService,
	domains *service.DomainService,
	records *service.DNSRecordService,
	accounts *service.CloudAccountService,
	jobs *service.SyncJobService,
	webhooks *WebhookHandler,
	config *service.ConfigService,
	apiRoutes *service.APIRouteAssetService,
	runtime *service.RuntimeControl,
	audit *auditservice.Service,
	cacheLayer *cache.Cache,
) *Handler {
	return &Handler{
		assets:    assets,
		ports:     ports,
		domains:   domains,
		records:   records,
		accounts:  accounts,
		jobs:      jobs,
		webhooks:  webhooks,
		config:    config,
		apiRoutes: apiRoutes,
		runtime:   runtime,
		audit:     audit,
		cache:     cacheLayer,
	}
}

func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	caasm := rg.Group("/caasm")
	caasm.GET("/assets", h.ListAssets)
	caasm.GET("/ip-assets", h.ListIPAssets)
	caasm.GET("/assets/services", h.GetServices)
	caasm.GET("/assets/environments", h.GetEnvironments)
	caasm.GET("/assets/:id", h.GetAsset)
	caasm.GET("/assets/:id/tags", h.GetAssetTags)
	caasm.GET("/dns-records/:id/comments", h.ListDNSRecordComments)
	caasm.GET("/ports", h.ListPorts)
	caasm.GET("/ports/stats", h.PortStats)
	caasm.GET("/ports/high-risk", h.GetHighRiskPorts)
	caasm.GET("/ports/:id", h.GetPort)
	caasm.GET("/assets/ports/search", h.SearchPorts) // alias per plan
	caasm.GET("/assets/ports/stats", h.PortStats)    // alias per plan
	caasm.GET("/dns-records", h.ListDNSRecords)
	caasm.GET("/domains", h.ListDomains)
	caasm.GET("/domains/stats", h.DomainStats)
	caasm.GET("/domains/registrars", h.GetRegistrars)
	caasm.GET("/relationships/graph", h.GetRelationshipGraph)
	caasm.GET("/config", h.GetConfig)
	caasm.GET("/runtime/status", h.GetRuntimeStatus)
}

func (h *Handler) RegisterManageRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	caasm := rg.Group("/caasm")
	caasm.PUT("/assets/:id/tags", h.UpdateAssetTags)
	caasm.PUT("/assets/:id/owner", h.UpdateAssetOwner)
	caasm.PUT("/dns-records/:id/owner", h.UpdateDNSRecordOwner)
	caasm.POST("/dns-records/:id/comments", h.AddDNSRecordComment)
	caasm.POST("/domains/:id/claim", h.ClaimDomain)
	caasm.PUT("/config", h.UpdateConfig)
	caasm.PUT("/config/tagging", h.UpdateTaggingRules)
	caasm.PUT("/config/ports", h.UpdatePortCatalog)
	caasm.PUT("/config/ephemeral", h.UpdateEphemeralPolicy)
	caasm.PUT("/config/discovery", h.UpdateDiscoveryIntervals)
	caasm.PUT("/config/sync", h.UpdateSyncSettings)
	caasm.POST("/runtime/start", h.StartRuntime)
	caasm.POST("/runtime/stop", h.StopRuntime)
	caasm.POST("/runtime/workers/start", h.StartWorkers)
	caasm.POST("/runtime/workers/stop", h.StopWorkers)
	caasm.POST("/runtime/scheduler/start", h.StartScheduler)
	caasm.POST("/runtime/scheduler/stop", h.StopScheduler)
	caasm.POST("/runtime/kill-switch/enable", h.EnableKillSwitch)
	caasm.POST("/runtime/kill-switch/disable", h.DisableKillSwitch)
}

func (h *Handler) RegisterIngestRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	caasm := rg.Group("/caasm")
	caasm.POST("/ingest", h.IngestData)
}

func (h *Handler) RegisterWebhookRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	// Keep webhook routes behind CAASM management permission until provider
	// signature verification is implemented.
	if h.webhooks != nil {
		h.webhooks.RegisterRoutes(rg)
	}
}

func (h *Handler) RegisterCloudAccountRoutes(viewGroup, manageGroup *gin.RouterGroup) {
	if h.accounts == nil || viewGroup == nil {
		return
	}
	viewGroup.GET("", h.ListCloudAccounts)
	viewGroup.GET("/:id", h.GetCloudAccount)
	if h.jobs != nil {
		viewGroup.GET("/:id/jobs", h.ListCloudAccountJobs)
	}
	if manageGroup != nil {
		manageGroup.POST("/secrets", h.CreateCloudAccountSecret)
		manageGroup.POST("", h.CreateCloudAccount)
		manageGroup.PUT("/:id", h.UpdateCloudAccount)
		manageGroup.DELETE("/:id", h.DeleteCloudAccount)
		manageGroup.POST("/:id/test", h.TestCloudAccount)
		manageGroup.POST("/:id/sync", h.TriggerCloudAccountSync)
	}
}

func (h *Handler) RegisterSyncJobRoutes(viewGroup, manageGroup, ingestGroup *gin.RouterGroup) {
	if h.jobs == nil || viewGroup == nil {
		return
	}
	viewGroup.GET("", h.ListSyncJobs)
	viewGroup.GET("/:id", h.GetSyncJob)
	viewGroup.GET("/:id/events", h.GetSyncJobEvents)
	if manageGroup != nil {
		manageGroup.POST("/:id/stop", h.StopSyncJob)
	}
	if ingestGroup != nil {
		// Lifecycle hooks for the serverless worker path.
		ingestGroup.POST("/:id/start", h.StartSyncJob)
		ingestGroup.POST("/:id/events", h.ReportSyncJobEvent)
		ingestGroup.POST("/:id/fail", h.FailSyncJob)
	}
}

func (h *Handler) RegisterAPIRouteAssetRoutes(viewGroup, manageGroup *gin.RouterGroup) {
	if h.apiRoutes == nil || viewGroup == nil {
		return
	}
	viewGroup.GET("", h.ListAPIRouteAssets)
	viewGroup.GET("/config/active", h.GetActiveAPIRouteConfig)
	if manageGroup != nil {
		manageGroup.POST("/config/preview", h.PreviewAPIRouteConfig)
		manageGroup.POST("/config", h.SaveAPIRouteConfig)
	}
}

func (h *Handler) StartRuntime(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.runtime == nil {
		response.Error(c, http.StatusServiceUnavailable, "runtime control not configured")
		return
	}
	status, err := h.runtime.Start()
	if err != nil {
		var killSwitchErr *service.KillSwitchError
		if errors.As(err, &killSwitchErr) {
			response.Error(c, http.StatusLocked, err.Error())
			return
		}
		response.Error(c, http.StatusServiceUnavailable, err.Error())
		return
	}
	response.Success(c, status)
}

func (h *Handler) StopRuntime(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.runtime == nil {
		response.Error(c, http.StatusServiceUnavailable, "runtime control not configured")
		return
	}
	status := h.runtime.Stop()
	response.Success(c, status)
}

func (h *Handler) StartWorkers(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.runtime == nil {
		response.Error(c, http.StatusServiceUnavailable, "runtime control not configured")
		return
	}
	status, err := h.runtime.StartWorkers()
	if err != nil {
		var killSwitchErr *service.KillSwitchError
		if errors.As(err, &killSwitchErr) {
			response.Error(c, http.StatusLocked, err.Error())
			return
		}
		response.Error(c, http.StatusServiceUnavailable, err.Error())
		return
	}
	response.Success(c, status)
}

func (h *Handler) StopWorkers(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.runtime == nil {
		response.Error(c, http.StatusServiceUnavailable, "runtime control not configured")
		return
	}
	status := h.runtime.StopWorkers()
	response.Success(c, status)
}

func (h *Handler) StartScheduler(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.runtime == nil {
		response.Error(c, http.StatusServiceUnavailable, "runtime control not configured")
		return
	}
	status, err := h.runtime.StartScheduler()
	if err != nil {
		var killSwitchErr *service.KillSwitchError
		if errors.As(err, &killSwitchErr) {
			response.Error(c, http.StatusLocked, err.Error())
			return
		}
		response.Error(c, http.StatusServiceUnavailable, err.Error())
		return
	}
	response.Success(c, status)
}

func (h *Handler) StopScheduler(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.runtime == nil {
		response.Error(c, http.StatusServiceUnavailable, "runtime control not configured")
		return
	}
	status := h.runtime.StopScheduler()
	response.Success(c, status)
}

func (h *Handler) EnableKillSwitch(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	status, err := h.setKillSwitch(c.Request.Context(), true)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, status)
}

func (h *Handler) DisableKillSwitch(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	status, err := h.setKillSwitch(c.Request.Context(), false)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, status)
}

func (h *Handler) GetRuntimeStatus(c *gin.Context) {
	status, err := h.runtimeStatus(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusServiceUnavailable, err.Error())
		return
	}
	response.Success(c, status)
}

func (h *Handler) setKillSwitch(ctx context.Context, enabled bool) (service.RuntimeStatus, error) {
	if h.config == nil {
		return service.RuntimeStatus{}, errors.New("config service not available")
	}
	if _, err := h.config.SetKillSwitch(ctx, enabled); err != nil {
		return service.RuntimeStatus{}, err
	}
	if h.runtime != nil {
		if enabled {
			h.runtime.Stop()
			return h.runtimeStatus(ctx)
		}
		return h.runtimeStatus(ctx)
	}
	return service.RuntimeStatus{
		RuntimeConfigured: false,
		KillSwitchEnabled: enabled,
	}, nil
}

func (h *Handler) runtimeStatus(ctx context.Context) (service.RuntimeStatus, error) {
	if h.runtime != nil {
		status := h.runtime.Status()
		if h.config != nil {
			state, err := h.config.KillSwitchEnabled(ctx)
			if err != nil {
				return service.RuntimeStatus{}, err
			}
			status.KillSwitchEnabled = state
		}
		return status, nil
	}
	enabled := false
	if h.config != nil {
		state, err := h.config.KillSwitchEnabled(ctx)
		if err != nil {
			return service.RuntimeStatus{}, err
		}
		enabled = state
	}
	return service.RuntimeStatus{
		RuntimeConfigured: false,
		KillSwitchEnabled: enabled,
	}, nil
}

// ListAssets lists assets.
//
//	@Summary		List assets
//	@Description	List assets of all kinds (IP/domain/service, etc.) with multi-dimensional filtering and ETag caching (If-None-Match -> 304).
//	@Tags			caasm
//	@Produce		json
//	@Param			page				query		int		false	"Page number, default 1"
//	@Param			page_size			query		int		false	"Page size, default 20, max 100"
//	@Param			type				query		string	false	"Asset type (AssetType)"
//	@Param			owner				query		string	false	"Owner email"
//	@Param			owner_employee_id	query		int		false	"Owner employee ID"
//	@Param			cloud_account_id	query		int		false	"Cloud account ID"
//	@Param			environment			query		string	false	"Environment"
//	@Param			status				query		string	false	"Asset status (AssetStatus)"
//	@Param			criticality			query		string	false	"Criticality"
//	@Param			provider			query		string	false	"Cloud provider (CloudProvider)"
//	@Param			service				query		string	false	"Service name"
//	@Param			q					query		string	false	"Keyword search"
//	@Success		200					{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are asset list entries"
//	@Security		agentApiKey
//	@Security		bearerAuth
//	@Router			/api/v1/caasm/assets [get]
func (h *Handler) ListAssets(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}

	filter := repository.AssetFilter{}
	if t := c.Query("type"); t != "" {
		filter.Type = caasmmodel.AssetType(t)
	}
	if owner := strings.TrimSpace(c.Query("owner")); owner != "" {
		filter.OwnerEmail = owner
	}
	if ownerEmployee := strings.TrimSpace(c.Query("owner_employee_id")); ownerEmployee != "" {
		if id, err := strconv.ParseUint(ownerEmployee, 10, 64); err == nil {
			filter.OwnerEmployeeID = &id
		}
	}
	if acct := c.Query("cloud_account_id"); acct != "" {
		if id, err := strconv.ParseUint(acct, 10, 64); err == nil {
			filter.CloudAcctID = id
		}
	}
	if env := c.Query("environment"); env != "" {
		filter.Environment = env
	}
	if status := c.Query("status"); status != "" {
		filter.Status = caasmmodel.AssetStatus(status)
	}
	if criticality := c.Query("criticality"); criticality != "" {
		filter.Criticality = criticality
	}
	if provider := c.Query("provider"); provider != "" {
		filter.Provider = caasmmodel.CloudProvider(provider)
	}
	if service := c.Query("service"); service != "" {
		filter.Service = service
	}
	if q := strings.TrimSpace(c.Query("q")); q != "" {
		filter.Search = q
	}

	userID := uint64(0)
	if id := currentUserID(c); id != nil {
		userID = *id
	}
	cacheKey := ""
	if h.cache != nil {
		cacheKey = "api:v1:caasm:assets:list:u" + strconv.FormatUint(userID, 10) + ":" + c.Request.URL.Query().Encode()
		if cached, err := h.cache.Get(c.Request.Context(), cacheKey); err == nil && cached != "" {
			if splitAt := strings.IndexByte(cached, '\n'); splitAt > 0 {
				etag := cached[:splitAt]
				body := cached[splitAt+1:]
				c.Header("ETag", etag)
				c.Header("Cache-Control", "private, max-age=30")
				if httpcache.IfNoneMatchMatches(c.GetHeader("If-None-Match"), etag) {
					c.Status(http.StatusNotModified)
					return
				}
				c.Data(http.StatusOK, "application/json", []byte(body))
				return
			}
		}
	}

	result, err := h.assets.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	items := make([]caasmAssetListItemDTO, 0, len(result.Items))
	itemFingerprints := make([]string, 0, len(result.Items))
	for _, asset := range result.Items {
		items = append(items, toCaasmAssetListItemDTO(asset))
		itemFingerprints = append(itemFingerprints, strconv.FormatUint(asset.ID, 10)+"@"+asset.UpdatedAt.UTC().Format(time.RFC3339Nano))
	}

	data := response.PaginatedResponse{
		Items:      items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	}
	etag := httpcache.Checksum(
		"caasm:assets:list",
		"u="+strconv.FormatUint(userID, 10),
		c.Request.URL.Query().Encode(),
		"total="+strconv.FormatInt(result.Total, 10),
		strings.Join(itemFingerprints, ","),
	)
	c.Header("ETag", etag)
	c.Header("Cache-Control", "private, max-age=30")
	if httpcache.IfNoneMatchMatches(c.GetHeader("If-None-Match"), etag) {
		c.Status(http.StatusNotModified)
		return
	}

	apiResp := response.APIResponse{
		Code:      0,
		Message:   "success",
		Data:      data,
		Timestamp: time.Now().Unix(),
	}
	encoded, err := json.Marshal(apiResp)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to encode response")
		return
	}
	if cacheKey != "" {
		_ = h.cache.Set(c.Request.Context(), cacheKey, etag+"\n"+string(encoded), 30*time.Second)
	}
	c.Data(http.StatusOK, "application/json", encoded)
}

// ListIPAssets lists IP assets.
//
//	@Summary	List IP assets
//	@Description	List IP assets with filtering by keyword, cloud provider, region, and exposure; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20, max 100"
//	@Param	q	query	string	false	"Keyword"
//	@Param	provider	query	string	false	"Cloud provider (CloudProvider)"
//	@Param	cloud_account_id	query	int	false	"Cloud account ID"
//	@Param	asset_service	query	string	false	"Asset service name"
//	@Param	region	query	string	false	"Region"
//	@Param	exposure	query	string	false	"Exposure, enum: public / private (other values return 400)"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are IP asset entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/ip-assets [get]
func (h *Handler) ListIPAssets(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}

	filter := repository.IPAssetFilter{
		Query:   strings.TrimSpace(c.Query("q")),
		Service: c.Query("asset_service"),
		Region:  c.Query("region"),
	}
	if provider := c.Query("provider"); provider != "" {
		filter.Provider = caasmmodel.CloudProvider(provider)
	}
	if cloudAccountID := c.Query("cloud_account_id"); cloudAccountID != "" {
		if id, err := strconv.ParseUint(cloudAccountID, 10, 64); err == nil {
			filter.CloudAccountID = id
		}
	}
	if exposure := c.Query("exposure"); exposure != "" {
		if exposure != "public" && exposure != "private" {
			response.Error(c, http.StatusBadRequest, "exposure must be public or private")
			return
		}
		isPublic := exposure == "public"
		filter.IsPublic = &isPublic
	}

	result, err := h.assets.ListIPAssets(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	items := make([]caasmIPAssetListItemDTO, 0, len(result.Items))
	for _, entry := range result.Items {
		items = append(items, toCaasmIPAssetListItemDTO(entry))
	}
	response.Success(c, response.PaginatedResponse{
		Items:      items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	})
}

// ListAPIRouteAssets lists API route assets.
//
//	@Summary	List API route assets
//	@Description	List API route assets with filtering by keyword, environment, domain, namespace, service, plugin, and route kind; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20, max 100"
//	@Param	q	query	string	false	"Keyword"
//	@Param	environment	query	string	false	"Environment"
//	@Param	domain	query	string	false	"Domain name"
//	@Param	namespace	query	string	false	"Namespace"
//	@Param	service_name	query	string	false	"Service name"
//	@Param	plugin	query	string	false	"Plugin name"
//	@Param	route_kind	query	string	false	"Route kind"
//	@Param	external	query	bool	false	"Filter by external route flag"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are API route asset entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/api-assets [get]
func (h *Handler) ListAPIRouteAssets(c *gin.Context) {
	if h.apiRoutes == nil {
		response.Error(c, http.StatusServiceUnavailable, "api route asset service not available")
		return
	}
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	if page <= 0 {
		page = 1
	}
	if pageSize <= 0 {
		pageSize = 20
	}
	if pageSize > 100 {
		pageSize = 100
	}

	filter := repository.APIRouteAssetFilter{
		Search:      strings.TrimSpace(c.Query("q")),
		Environment: strings.TrimSpace(c.Query("environment")),
		Domain:      strings.TrimSpace(c.Query("domain")),
		Namespace:   strings.TrimSpace(c.Query("namespace")),
		ServiceName: strings.TrimSpace(c.Query("service_name")),
		Plugin:      strings.TrimSpace(c.Query("plugin")),
		RouteKind:   strings.TrimSpace(c.Query("route_kind")),
	}
	if external := strings.TrimSpace(c.Query("external")); external != "" {
		if parsed, err := strconv.ParseBool(external); err == nil {
			filter.External = &parsed
		}
	}

	result, err := h.apiRoutes.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, response.PaginatedResponse{
		Items:      toCAASMAPIRouteAssetDTOs(result.Items),
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	})
}

// GetActiveAPIRouteConfig returns the active API route config.
//
//	@Summary	Get the active API route config
//	@Description	Return the currently active API asset discovery config.
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=caasmAPIRouteConfigSummaryDTO}	"Success; data is the active API route config"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/api-assets/config/active [get]
func (h *Handler) GetActiveAPIRouteConfig(c *gin.Context) {
	if h.apiRoutes == nil {
		response.Error(c, http.StatusServiceUnavailable, "api route asset service not available")
		return
	}
	summary, err := h.apiRoutes.ActiveConfig(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, toCAASMAPIRouteConfigSummaryDTO(summary))
}

func (h *Handler) PreviewAPIRouteConfig(c *gin.Context) {
	if h.apiRoutes == nil {
		response.Error(c, http.StatusServiceUnavailable, "api route asset service not available")
		return
	}
	var req caasmAPIRouteConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid request body")
		return
	}
	preview, err := h.apiRoutes.Preview(c.Request.Context(), service.APIRouteConfigInput{
		Name:    req.Name,
		RawYAML: req.RawYAML,
		ActorID: currentUserID(c),
	})
	if err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	response.Success(c, toCAASMAPIRoutePreviewDTO(preview))
}

func (h *Handler) SaveAPIRouteConfig(c *gin.Context) {
	if h.apiRoutes == nil {
		response.Error(c, http.StatusServiceUnavailable, "api route asset service not available")
		return
	}
	var req caasmAPIRouteConfigRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid request body")
		return
	}
	summary, err := h.apiRoutes.SaveActiveConfig(c.Request.Context(), service.APIRouteConfigInput{
		Name:    req.Name,
		RawYAML: req.RawYAML,
		ActorID: currentUserID(c),
	})
	if err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	response.Success(c, toCAASMAPIRouteConfigSummaryDTO(summary))
}

// GetServices lists asset services.
//
//	@Summary	List asset services
//	@Description	Return the set of service names that assets belong to, for filter dropdowns.
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=[]string}	"Success; data is an array of service names"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/assets/services [get]
func (h *Handler) GetServices(c *gin.Context) {
	services, err := h.assets.GetDistinctServices(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, services)
}

// GetEnvironments lists environments.
//
//	@Summary	List environments
//	@Description	Return the set of asset environment values, for filter dropdowns.
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=[]string}	"Success; data is an array of environment names"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/assets/environments [get]
func (h *Handler) GetEnvironments(c *gin.Context) {
	environments, err := h.assets.GetDistinctEnvironments(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, environments)
}

// GetAsset returns the asset detail.
//
//	@Summary	Get asset detail
//	@Description	Get details of a single asset by ID.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Asset ID"
//	@Success	200	{object}	response.APIResponse	"Success; data is the asset detail object (including asset, ip_addresses, domain_assets, relationships)"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/assets/{id} [get]
func (h *Handler) GetAsset(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	detail, err := h.assets.Detail(c.Request.Context(), id)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, detail)
}

type UpdateOwnerRequest struct {
	OwnerEmployeeID *uint64 `json:"owner_employee_id"`
	DepartmentID    *string `json:"department_id"`
}

func (h *Handler) UpdateAssetOwner(c *gin.Context) {
	idStr := c.Param("id")
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid asset id")
		return
	}

	actorID := currentUserID(c)
	if actorID == nil || *actorID == 0 {
		response.Error(c, http.StatusForbidden, "not authorized to update asset owner")
		return
	}

	var req UpdateOwnerRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}

	if err := h.assets.UpdateOwner(c.Request.Context(), *actorID, id, req.OwnerEmployeeID, req.DepartmentID); err != nil {
		response.Error(c, caasmAuthorizationStatus(err), err.Error())
		return
	}

	h.invalidateAssetListCaches(c.Request.Context())
	response.Success(c, nil)
}

func (h *Handler) UpdateDNSRecordOwner(c *gin.Context) {
	if h.records == nil {
		response.Error(c, http.StatusServiceUnavailable, "dns record service not configured")
		return
	}

	idStr := c.Param("id")
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid record id")
		return
	}

	type recordOwnerRequest struct {
		OwnerEmail        string  `json:"owner_email" binding:"omitempty,email"`
		OwnerDepartmentID string  `json:"owner_department_id" binding:"required"`
		OwnerEmployeeID   *uint64 `json:"owner_employee_id"`
		IgnoreChanges     *bool   `json:"ignore_changes"`
		SuppressPattern   *string `json:"suppress_pattern"`
	}

	var req recordOwnerRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}

	actorID := currentUserID(c)
	if actorID == nil || *actorID == 0 {
		response.Error(c, http.StatusForbidden, "not authorized to update dns record owner")
		return
	}

	if err := h.records.UpdateOwner(c.Request.Context(), *actorID, id, req.OwnerEmail, req.OwnerDepartmentID, req.OwnerEmployeeID, req.IgnoreChanges, req.SuppressPattern); err != nil {
		response.Error(c, caasmAuthorizationStatus(err), err.Error())
		return
	}

	h.logAudit(c, "caasm.dns_record.owner.update", strconv.FormatUint(id, 10), nil, map[string]interface{}{
		"owner_email":         req.OwnerEmail,
		"owner_department_id": req.OwnerDepartmentID,
		"owner_employee_id":   req.OwnerEmployeeID,
	})
	h.invalidateAssetListCaches(c.Request.Context())
	response.Success(c, nil)
}

// ListPorts lists ports.
//
//	@Summary	List ports
//	@Description	List ports (IP/domain port index) with filtering by port number, protocol, status, exposure, and service; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Param	port	query	int	false	"Port number"
//	@Param	protocol	query	string	false	"Protocol (tcp/udp)"
//	@Param	status	query	string	false	"Port status"
//	@Param	exposure	query	string	false	"Exposure"
//	@Param	service	query	string	false	"Service name"
//	@Param	asset_service	query	string	false	"Asset service name"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are port entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/ports [get]
func (h *Handler) ListPorts(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	filter := repository.PortQueryFilter{}
	if portStr := c.Query("port"); portStr != "" {
		if p, err := strconv.ParseUint(portStr, 10, 32); err == nil {
			port := uint32(p)
			filter.Port = &port
		}
	}
	filter.Protocol = c.Query("protocol")
	filter.Status = c.Query("status")
	filter.Exposure = c.Query("exposure")
	filter.Service = c.Query("service")
	filter.AssetService = c.Query("asset_service")

	result, err := h.ports.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	items := make([]caasmPortListItemDTO, 0, len(result.Items))
	for _, entry := range result.Items {
		items = append(items, toCaasmPortListItemDTO(entry))
	}

	response.Success(c, response.PaginatedResponse{
		Items:      items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	})
}

// GetPort returns the port detail.
//
//	@Summary	Get port detail
//	@Description	Get a single port by ID (including evidence).
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Port ID"
//	@Success	200	{object}	response.APIResponse{data=caasmPortDetailDTO}	"Success; data is the port detail"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/ports/{id} [get]
func (h *Handler) GetPort(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid port id")
		return
	}
	detail, err := h.ports.Detail(c.Request.Context(), id)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			response.Error(c, http.StatusNotFound, "port not found")
			return
		}
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, toCaasmPortDetailDTO(detail))
}

// SearchPorts is an alias to ListPorts to satisfy the plan's `/assets/ports/search` endpoint.
// SearchPorts searches asset ports.
//
//	@Summary	Search asset ports
//	@Description	Semantic alias of ListPorts; parameters and response are identical to GET /api/v1/caasm/ports.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Param	port	query	int	false	"Port number"
//	@Param	protocol	query	string	false	"Protocol (tcp/udp)"
//	@Param	status	query	string	false	"Port status"
//	@Param	exposure	query	string	false	"Exposure"
//	@Param	service	query	string	false	"Service name"
//	@Param	asset_service	query	string	false	"Asset service name"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are port entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/assets/ports/search [get]
func (h *Handler) SearchPorts(c *gin.Context) {
	h.ListPorts(c)
}

// PortStats returns port statistics.
//
//	@Summary	Get port statistics
//	@Description	Return port statistics (total, public/private, status distribution, top open ports).
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=service.PortStatsResult}	"Success; data is the port statistics object"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/ports/stats [get]
func (h *Handler) PortStats(c *gin.Context) {
	stats, err := h.ports.Stats(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, stats)
}

// GetHighRiskPorts lists high-risk ports.
//
//	@Summary	List high-risk ports
//	@Description	Return high-risk ports.
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=[]caasmPortListItemDTO}	"Success; data is an array of high-risk ports"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/ports/high-risk [get]
func (h *Handler) GetHighRiskPorts(c *gin.Context) {
	ports, err := h.ports.GetHighRiskPorts(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	items := make([]caasmPortListItemDTO, 0, len(ports))
	for _, entry := range ports {
		items = append(items, toCaasmPortListItemDTO(entry))
	}
	response.Success(c, items)
}

func (h *Handler) ExportPorts(c *gin.Context) {
	if h.ports == nil {
		response.Error(c, http.StatusServiceUnavailable, "port service not configured")
		return
	}
	lines, err := h.ports.Export(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	filename := "caasm-ports-" + time.Now().UTC().Format("20060102T150405Z") + ".txt"
	c.Header("Content-Type", "text/plain; charset=utf-8")
	c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
	c.String(http.StatusOK, strings.Join(lines, "\n"))
}

// ListDNSRecords lists DNS records.
//
//	@Summary	List DNS records
//	@Description	List DNS records with filtering by keyword, record type, owner department, domain, record value, and alert presence; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Param	q	query	string	false	"Keyword"
//	@Param	type	query	string	false	"DNS record type (A/CNAME, etc.)"
//	@Param	owner_department_id	query	string	false	"Owner department ID"
//	@Param	domain_id	query	int	false	"Domain ID"
//	@Param	domain_name	query	string	false	"Domain name"
//	@Param	value	query	string	false	"Record value fuzzy match"
//	@Param	has_alert	query	bool	false	"Filter by alert presence"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are DNS record entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/dns-records [get]
func (h *Handler) ListDNSRecords(c *gin.Context) {
	if h.records == nil {
		response.Error(c, http.StatusServiceUnavailable, "dns record service not configured")
		return
	}

	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	filter := repository.DNSRecordFilter{
		Query: strings.TrimSpace(c.Query("q")),
		Type:  c.Query("type"),
	}
	if dept := strings.TrimSpace(c.Query("owner_department_id")); dept != "" {
		filter.OwnerDept = dept
	}
	if domainID := c.Query("domain_id"); domainID != "" {
		if id, err := strconv.ParseUint(domainID, 10, 64); err == nil {
			filter.DomainID = id
		}
	}
	if domainName := strings.TrimSpace(c.Query("domain_name")); domainName != "" {
		filter.Domain = domainName
	}
	if value := strings.TrimSpace(c.Query("value")); value != "" {
		filter.ValueLike = value
	}
	if hasAlertStr := c.Query("has_alert"); hasAlertStr != "" {
		if hasAlertStr == "true" || hasAlertStr == "1" {
			hasAlert := true
			filter.HasAlert = &hasAlert
		} else if hasAlertStr == "false" || hasAlertStr == "0" {
			hasAlert := false
			filter.HasAlert = &hasAlert
		}
	}

	userID := currentCAASMCacheUserID(c)
	cacheKey := ""
	if h.cache != nil {
		cacheKey = "api:v1:caasm:dns-records:list:u" + strconv.FormatUint(userID, 10) + ":" + c.Request.URL.Query().Encode()
		if h.serveCachedResponse(c, cacheKey, 30*time.Second) {
			return
		}
	}

	result, err := h.records.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	itemFingerprints := make([]string, 0, len(result.Items))
	for _, record := range result.Items {
		itemFingerprints = append(itemFingerprints, strconv.FormatUint(record.ID, 10)+"@"+record.UpdatedAt.UTC().Format(time.RFC3339Nano))
	}
	data := response.PaginatedResponse{
		Items:      result.Items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	}
	etag := httpcache.Checksum(
		"caasm:dns-records:list",
		"u="+strconv.FormatUint(userID, 10),
		c.Request.URL.Query().Encode(),
		"total="+strconv.FormatInt(result.Total, 10),
		strings.Join(itemFingerprints, ","),
	)
	h.writeCachedResponse(c, cacheKey, 30*time.Second, etag, data)
}

// ListDNSRecordComments returns DNS record comments.
//
//	@Summary	Get DNS record comments
//	@Description	Get comments of a DNS record by ID.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"DNS record ID"
//	@Success	200	{object}	response.APIResponse	"Success; data is an array of comments (caasmmodel.DNSRecordComment)"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/dns-records/{id}/comments [get]
func (h *Handler) ListDNSRecordComments(c *gin.Context) {
	if h.records == nil {
		response.Error(c, http.StatusServiceUnavailable, "dns record service not configured")
		return
	}
	recordID, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || recordID == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	comments, err := h.records.ListComments(c.Request.Context(), recordID)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, comments)
}

func (h *Handler) AddDNSRecordComment(c *gin.Context) {
	if h.records == nil {
		response.Error(c, http.StatusServiceUnavailable, "dns record service not configured")
		return
	}
	recordID, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || recordID == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	var payload struct {
		Content string `json:"content" binding:"required"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil || strings.TrimSpace(payload.Content) == "" {
		response.Error(c, http.StatusBadRequest, "invalid payload")
		return
	}
	actor := currentActorName(c)
	comment, err := h.records.AddComment(c.Request.Context(), recordID, actor, payload.Content)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	h.logAudit(c, "caasm.dns_record.comment_added", strconv.FormatUint(recordID, 10), nil, comment)
	response.Success(c, comment)
}

// ListDomains lists domains.
//
//	@Summary	List domains
//	@Description	List domain assets with filtering by keyword, environment, status, registrar, etc.; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Param	q	query	string	false	"Keyword"
//	@Param	environment	query	string	false	"Environment"
//	@Param	status	query	string	false	"Domain status"
//	@Param	zone_type	query	string	false	"Zone type"
//	@Param	domain_class	query	string	false	"Domain class"
//	@Param	registrar	query	string	false	"Registrar"
//	@Param	provider	query	string	false	"Cloud provider (CloudProvider)"
//	@Param	forwarding	query	bool	false	"Filter by forwarding presence"
//	@Param	peering	query	bool	false	"Filter by peering presence"
//	@Param	auto_renew	query	bool	false	"Filter by auto-renew flag"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are domain entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/domains [get]
func (h *Handler) ListDomains(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))

	filter := repository.DomainFilter{
		Query:       strings.TrimSpace(c.Query("q")),
		Environment: c.Query("environment"),
		Status:      c.Query("status"),
		ZoneType:    c.Query("zone_type"),
		DomainClass: c.Query("domain_class"),
		Registrar:   strings.TrimSpace(c.Query("registrar")),
	}
	if provider := c.Query("provider"); provider != "" {
		filter.Provider = caasmmodel.CloudProvider(provider)
	}
	if val := c.Query("forwarding"); val != "" {
		forwarding := val == "true" || val == "1"
		filter.HasForwarding = &forwarding
	}
	if val := c.Query("peering"); val != "" {
		peering := val == "true" || val == "1"
		filter.HasPeering = &peering
	}
	if val := c.Query("auto_renew"); val != "" {
		autoRenew := val == "true" || val == "1"
		filter.AutoRenew = &autoRenew
	}

	userID := currentCAASMCacheUserID(c)
	cacheKey := ""
	if h.cache != nil {
		cacheKey = "api:v1:caasm:domains:list:u" + strconv.FormatUint(userID, 10) + ":" + c.Request.URL.Query().Encode()
		if h.serveCachedResponse(c, cacheKey, 30*time.Second) {
			return
		}
	}

	result, err := h.domains.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	itemFingerprints := make([]string, 0, len(result.Items))
	for _, domain := range result.Items {
		itemFingerprints = append(itemFingerprints, strconv.FormatUint(domain.ID, 10)+"@"+domain.UpdatedAt.UTC().Format(time.RFC3339Nano))
	}
	data := response.PaginatedResponse{
		Items:      result.Items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	}
	etag := httpcache.Checksum(
		"caasm:domains:list",
		"u="+strconv.FormatUint(userID, 10),
		c.Request.URL.Query().Encode(),
		"total="+strconv.FormatInt(result.Total, 10),
		strings.Join(itemFingerprints, ","),
	)
	h.writeCachedResponse(c, cacheKey, 30*time.Second, etag, data)
}

// DomainStats returns domain statistics.
//
//	@Summary	Get domain statistics
//	@Description	Return domain statistics (total, expiring soon, expired).
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=service.DomainStatsResult}	"Success; data is the domain statistics object"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/domains/stats [get]
func (h *Handler) DomainStats(c *gin.Context) {
	userID := currentCAASMCacheUserID(c)
	cacheKey := ""
	if h.cache != nil {
		cacheKey = "api:v1:caasm:domains:stats:u" + strconv.FormatUint(userID, 10)
		if h.serveCachedResponse(c, cacheKey, 30*time.Second) {
			return
		}
	}
	stats, err := h.domains.Stats(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	etag := httpcache.Checksum(
		"caasm:domains:stats",
		"u="+strconv.FormatUint(userID, 10),
		strconv.FormatInt(stats.Total, 10),
		strconv.FormatInt(stats.ExpiringSoon, 10),
		strconv.FormatInt(stats.Expired, 10),
	)
	h.writeCachedResponse(c, cacheKey, 30*time.Second, etag, stats)
}

// GetRegistrars lists domain registrars.
//
//	@Summary	List domain registrars
//	@Description	Return the set of domain registrar names, for filter dropdowns.
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=[]string}	"Success; data is an array of registrar names"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/domains/registrars [get]
func (h *Handler) GetRegistrars(c *gin.Context) {
	userID := currentCAASMCacheUserID(c)
	cacheKey := ""
	if h.cache != nil {
		cacheKey = "api:v1:caasm:domains:registrars:u" + strconv.FormatUint(userID, 10)
		if h.serveCachedResponse(c, cacheKey, 30*time.Second) {
			return
		}
	}
	registrars, err := h.domains.GetRegistrars(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	etag := httpcache.Checksum(
		"caasm:domains:registrars",
		"u="+strconv.FormatUint(userID, 10),
		strings.Join(registrars, ","),
	)
	h.writeCachedResponse(c, cacheKey, 30*time.Second, etag, registrars)
}

func (h *Handler) ClaimDomain(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	var payload struct {
		OwnerEmployeeID *uint64 `json:"owner_employee_id"`
		DepartmentID    *string `json:"department_id"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid request body")
		return
	}
	if payload.OwnerEmployeeID == nil {
		response.Error(c, http.StatusBadRequest, "owner_employee_id is required")
		return
	}
	actorID := currentUserID(c)
	if actorID == nil || *actorID == 0 {
		response.Error(c, http.StatusForbidden, "not authorized to claim domain owner")
		return
	}

	if err := h.domains.Claim(c.Request.Context(), *actorID, id, payload.OwnerEmployeeID, payload.DepartmentID); err != nil {
		response.Error(c, caasmAuthorizationStatus(err), err.Error())
		return
	}

	// Audit log
	h.logAudit(c, "caasm.domain.claim", strconv.FormatUint(id, 10), nil, map[string]interface{}{
		"owner_employee_id": payload.OwnerEmployeeID,
		"department_id":     payload.DepartmentID,
	})
	h.invalidateAssetListCaches(c.Request.Context())
	response.Success(c, gin.H{"success": true})
}

// ListCloudAccounts lists cloud accounts.
//
//	@Summary	List cloud accounts
//	@Description	List cloud accounts with filtering by keyword, provider, and status; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Param	q	query	string	false	"Keyword"
//	@Param	provider	query	string	false	"Cloud provider (CloudProvider)"
//	@Param	status	query	string	false	"Cloud account status (CloudAccountStatus)"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are cloud account entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/cloud-accounts [get]
func (h *Handler) ListCloudAccounts(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	filter := repository.CloudAccountFilter{
		Query: strings.TrimSpace(c.Query("q")),
	}
	if provider := c.Query("provider"); provider != "" {
		filter.Provider = caasmmodel.CloudProvider(provider)
	}
	if status := c.Query("status"); status != "" {
		filter.Status = caasmmodel.CloudAccountStatus(status)
	}

	result, err := h.accounts.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	items := make([]cloudAccountDTO, 0, len(result.Items))
	for i := range result.Items {
		account := result.Items[i]
		var resolution *service.ThrottlingResolution
		if account.Provider == caasmmodel.CloudProviderAWS {
			if resolved, resolveErr := h.accounts.ResolveAccountThrottling(c.Request.Context(), &account); resolveErr == nil {
				resolution = &resolved
			}
		}
		items = append(items, toCloudAccountDTO(&account, resolution))
	}
	response.Success(c, response.PaginatedResponse{
		Items:      items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	})
}

// GetCloudAccount returns the cloud account detail.
//
//	@Summary	Get cloud account detail
//	@Description	Get a single cloud account by ID.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Cloud account ID"
//	@Success	200	{object}	response.APIResponse{data=cloudAccountDTO}	"Success; data is the cloud account detail"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/cloud-accounts/{id} [get]
func (h *Handler) GetCloudAccount(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	account, err := h.accounts.Get(c.Request.Context(), id)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	var resolution *service.ThrottlingResolution
	if account.Provider == caasmmodel.CloudProviderAWS {
		if resolved, resolveErr := h.accounts.ResolveAccountThrottling(c.Request.Context(), account); resolveErr == nil {
			resolution = &resolved
		}
	}
	response.Success(c, toCloudAccountDTO(account, resolution))
}

func (h *Handler) CreateCloudAccount(c *gin.Context) {
	var payload struct {
		Name       string                 `json:"name" binding:"required"`
		Provider   string                 `json:"provider" binding:"required"`
		ExternalID string                 `json:"external_id" binding:"required"`
		Status     string                 `json:"status"`
		Metadata   map[string]interface{} `json:"metadata"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}

	account := &caasmmodel.CloudAccount{
		Name:       strings.TrimSpace(payload.Name),
		Provider:   caasmmodel.CloudProvider(strings.TrimSpace(payload.Provider)),
		ExternalID: strings.TrimSpace(payload.ExternalID),
		Status:     caasmmodel.CloudAccountStatus(payload.Status),
		Metadata:   toJSONB(payload.Metadata),
	}
	if account.Status == "" {
		account.Status = caasmmodel.CloudAccountStatusActive
	}

	if err := h.accounts.Create(c.Request.Context(), account); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	h.logAudit(c, "caasm.cloud_account.create", strconv.FormatUint(account.ID, 10), nil, account)
	var resolution *service.ThrottlingResolution
	if account.Provider == caasmmodel.CloudProviderAWS {
		if resolved, resolveErr := h.accounts.ResolveAccountThrottling(c.Request.Context(), account); resolveErr == nil {
			resolution = &resolved
		}
	}
	response.Success(c, toCloudAccountDTO(account, resolution))
}

func (h *Handler) CreateCloudAccountSecret(c *gin.Context) {
	if h.accounts == nil {
		response.Error(c, http.StatusServiceUnavailable, "cloud account service not configured")
		return
	}
	var payload struct {
		Provider  string `json:"provider"`
		Name      string `json:"name"`
		Region    string `json:"region"`
		APIKey    string `json:"api_key" binding:"required"`
		APISecret string `json:"api_secret" binding:"required"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	provider := payload.Provider
	if provider == "" {
		provider = string(caasmmodel.CloudProviderGoDaddy)
	}

	arn, err := h.accounts.CreateProviderSecret(c.Request.Context(), caasmmodel.CloudProvider(provider), payload.Name, payload.Region, payload.APIKey, payload.APISecret)
	if err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	h.logAudit(c, "caasm.cloud_account.secret.create", provider, nil, map[string]string{"secret_arn": arn})
	response.Success(c, gin.H{"secret_arn": arn})
}

func (h *Handler) UpdateCloudAccount(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	var payload struct {
		Name       string                 `json:"name"`
		Provider   string                 `json:"provider"`
		ExternalID string                 `json:"external_id"`
		Status     string                 `json:"status"`
		Metadata   map[string]interface{} `json:"metadata"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}

	// Fetch existing account to preserve timestamps
	account, err := h.accounts.Get(c.Request.Context(), id)
	if err != nil {
		response.Error(c, http.StatusNotFound, "account not found")
		return
	}

	// Keep a copy for audit log
	before := *account

	// Update fields
	if name := strings.TrimSpace(payload.Name); name != "" {
		account.Name = name
	}
	if provider := strings.TrimSpace(payload.Provider); provider != "" {
		account.Provider = caasmmodel.CloudProvider(provider)
	}
	if externalID := strings.TrimSpace(payload.ExternalID); externalID != "" {
		account.ExternalID = externalID
	}
	if payload.Status != "" {
		account.Status = caasmmodel.CloudAccountStatus(payload.Status)
	}
	if payload.Metadata != nil {
		account.Metadata = toJSONB(payload.Metadata)
	}

	if err := h.accounts.Update(c.Request.Context(), account); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	h.logAudit(c, "caasm.cloud_account.update", strconv.FormatUint(id, 10), before, account)
	var resolution *service.ThrottlingResolution
	if account.Provider == caasmmodel.CloudProviderAWS {
		if resolved, resolveErr := h.accounts.ResolveAccountThrottling(c.Request.Context(), account); resolveErr == nil {
			resolution = &resolved
		}
	}
	response.Success(c, toCloudAccountDTO(account, resolution))
}

func (h *Handler) DeleteCloudAccount(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	before, _ := h.accounts.Get(c.Request.Context(), id)
	if err := h.accounts.Delete(c.Request.Context(), id); err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	h.logAudit(c, "caasm.cloud_account.delete", strconv.FormatUint(id, 10), before, nil)
	response.Success(c, gin.H{"success": true})
}

func (h *Handler) TestCloudAccount(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	result, err := h.accounts.Test(c.Request.Context(), id)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	h.logAudit(c, "caasm.cloud_account.test", strconv.FormatUint(id, 10), nil, result)
	response.Success(c, result)
}

func (h *Handler) TriggerCloudAccountSync(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	userID := currentUserID(c)
	result, err := h.accounts.TriggerSync(c.Request.Context(), id, userID)
	if err != nil {
		// Check if it's a backoff error and return 429 (Too Many Requests)
		if backoffErr, ok := err.(*service.BackoffError); ok {
			retryAfter := int64(time.Until(backoffErr.RetryAt).Seconds())
			if retryAfter < 0 {
				retryAfter = 0
			}
			c.Header("Retry-After", strconv.FormatInt(retryAfter, 10))
			response.Error(c, http.StatusTooManyRequests, err.Error())
			return
		}
		var killSwitchErr *service.KillSwitchError
		if errors.As(err, &killSwitchErr) {
			response.Error(c, http.StatusLocked, err.Error())
			return
		}
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	h.logAudit(c, "caasm.cloud_account.sync", strconv.FormatUint(id, 10), nil, result)
	response.Success(c, result)
}

// GetAssetTags returns the asset tags.
//
//	@Summary	Get asset tags
//	@Description	Get the tag key-value set of an asset by ID.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Asset ID"
//	@Success	200	{object}	response.APIResponse{data=map[string]string}	"Success; data is the asset tag key-value set"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/assets/{id}/tags [get]
func (h *Handler) GetAssetTags(c *gin.Context) {
	assetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || assetID == 0 {
		response.Error(c, http.StatusBadRequest, "invalid asset id")
		return
	}
	tags, err := h.accounts.GetTags(c.Request.Context(), assetID)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, tags)
}

func (h *Handler) UpdateAssetTags(c *gin.Context) {
	assetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || assetID == 0 {
		response.Error(c, http.StatusBadRequest, "invalid asset id")
		return
	}
	var payload map[string]string
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid tags payload")
		return
	}
	before, _ := h.accounts.GetTags(c.Request.Context(), assetID)
	if err := h.accounts.ReplaceTags(c.Request.Context(), assetID, payload); err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	h.logAudit(c, "caasm.asset.tags.update", strconv.FormatUint(assetID, 10), before, payload)
	h.invalidateAssetListCaches(c.Request.Context())
	response.Success(c, payload)
}

// ListCloudAccountJobs lists sync jobs of a cloud account.
//
//	@Summary	List cloud account sync jobs
//	@Description	List sync jobs of a cloud account by ID, paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Cloud account ID"
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are sync job entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/cloud-accounts/{id}/jobs [get]
func (h *Handler) ListCloudAccountJobs(c *gin.Context) {
	if h.jobs == nil {
		response.Success(c, response.PaginatedResponse{Items: []interface{}{}, Total: 0})
		return
	}
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	filter := repository.SyncJobFilter{
		CloudAccountID: &id,
		JobType:        "cloud_account_sync",
	}
	result, err := h.jobs.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, response.PaginatedResponse{
		Items:      result.Items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	})
}

// ListSyncJobs lists sync jobs.
//
//	@Summary	List sync jobs
//	@Description	List sync jobs with filtering by job type, status, cloud account, and execution mode; paginated.
//	@Tags	caasm
//	@Produce	json
//	@Param	page	query	int	false	"Page number, default 1"
//	@Param	page_size	query	int	false	"Page size, default 20"
//	@Param	job_type	query	string	false	"Job type"
//	@Param	status	query	string	false	"Sync job status (SyncJobStatus)"
//	@Param	cloud_account_id	query	int	false	"Cloud account ID"
//	@Param	execution_mode	query	string	false	"Execution mode"
//	@Success	200	{object}	response.APIResponse{data=response.PaginatedResponse}	"Success; data is a paginated structure whose items are sync job entries"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/sync-jobs [get]
func (h *Handler) ListSyncJobs(c *gin.Context) {
	page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
	pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
	filter := repository.SyncJobFilter{
		JobType: c.Query("job_type"),
	}
	if status := c.Query("status"); status != "" {
		filter.Status = caasmmodel.SyncJobStatus(status)
	}
	if accountID := c.Query("cloud_account_id"); accountID != "" {
		if id, err := strconv.ParseUint(accountID, 10, 64); err == nil {
			filter.CloudAccountID = &id
		}
	}
	if mode := c.Query("execution_mode"); mode != "" {
		filter.ExecutionMode = mode
	}
	result, err := h.jobs.List(c.Request.Context(), filter, page, pageSize)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, response.PaginatedResponse{
		Items:      result.Items,
		Total:      result.Total,
		Page:       page,
		PageSize:   pageSize,
		TotalPages: (int(result.Total) + pageSize - 1) / pageSize,
	})
}

// GetSyncJob returns the sync job detail.
//
//	@Summary	Get sync job detail
//	@Description	Get a single sync job by ID.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Sync job ID"
//	@Success	200	{object}	response.APIResponse	"Success; data is the sync job detail (caasmmodel.SyncJob)"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/sync-jobs/{id} [get]
func (h *Handler) GetSyncJob(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	job, err := h.jobs.Get(c.Request.Context(), id)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, job)
}

// GetSyncJobEvents lists sync job events.
//
//	@Summary	List sync job events
//	@Description	Get the event stream of a sync job by ID, supporting limit and before_id/after_id cursors.
//	@Tags	caasm
//	@Produce	json
//	@Param	id	path	int	true	"Sync job ID"
//	@Param	limit	query	int	false	"Maximum number of events to return"
//	@Param	before_id	query	int	false	"Return only events before this event ID (mutually exclusive with after_id)"
//	@Param	after_id	query	int	false	"Return only events after this event ID (mutually exclusive with before_id)"
//	@Success	200	{object}	response.APIResponse	"Success; data is an array of sync job events (caasmmodel.SyncJobEvent)"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/sync-jobs/{id}/events [get]
func (h *Handler) GetSyncJobEvents(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	opts, err := parseSyncJobEventsQuery(c)
	if err != nil {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	events, err := h.jobs.ListEvents(c.Request.Context(), id, opts)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, events)
}

func parseSyncJobEventsQuery(c *gin.Context) (repository.SyncJobEventListOptions, error) {
	var opts repository.SyncJobEventListOptions

	if rawLimit := strings.TrimSpace(c.Query("limit")); rawLimit != "" {
		limit, err := strconv.Atoi(rawLimit)
		if err != nil || limit <= 0 {
			return repository.SyncJobEventListOptions{}, errors.New("invalid limit")
		}
		if limit > repository.MaxSyncJobEventLimit {
			limit = repository.MaxSyncJobEventLimit
		}
		opts.Limit = limit
	}

	if rawBefore := strings.TrimSpace(c.Query("before_id")); rawBefore != "" {
		beforeID, err := strconv.ParseUint(rawBefore, 10, 64)
		if err != nil || beforeID == 0 {
			return repository.SyncJobEventListOptions{}, errors.New("invalid before_id")
		}
		opts.BeforeID = &beforeID
	}

	if rawAfter := strings.TrimSpace(c.Query("after_id")); rawAfter != "" {
		afterID, err := strconv.ParseUint(rawAfter, 10, 64)
		if err != nil || afterID == 0 {
			return repository.SyncJobEventListOptions{}, errors.New("invalid after_id")
		}
		opts.AfterID = &afterID
	}

	if opts.BeforeID != nil && opts.AfterID != nil {
		return repository.SyncJobEventListOptions{}, errors.New("before_id and after_id cannot be used together")
	}
	return opts, nil
}

func (h *Handler) StopSyncJob(c *gin.Context) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}
	if err := h.jobs.Stop(c.Request.Context(), id); err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	if h.runtime != nil {
		h.runtime.CancelSyncJob(id)
	}
	if h.accounts != nil {
		h.accounts.CancelSyncJob(id)
	}
	response.Success(c, gin.H{"success": true})
}

// GetRelationshipGraph returns the asset relationship graph.
//
//	@Summary	Get asset relationship graph
//	@Description	Return relationship graph data (nodes + edges) among assets, domains, IPs, and ports.
//	@Tags	caasm
//	@Produce	json
//	@Success	200	{object}	response.APIResponse{data=service.RelationshipGraph}	"Success; data is the relationship graph (nodes + edges)"
//	@Security	agentApiKey
//	@Security	bearerAuth
//	@Router	/api/v1/caasm/relationships/graph [get]
func (h *Handler) GetRelationshipGraph(c *gin.Context) {
	if h.assets == nil {
		response.Error(c, http.StatusServiceUnavailable, "relationship graph is unavailable")
		return
	}
	graph, err := h.assets.GetRelationshipGraph(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, graph)
}

func (h *Handler) GetConfig(c *gin.Context) {
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	cfg, err := h.config.Get(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	resolved, err := h.config.ResolveEffectiveThrottling(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, toCaasmConfigResponseDTO(cfg.Data, resolved))
}

func (h *Handler) UpdateConfig(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	var req map[string]interface{}
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid request body")
		return
	}
	cfg, err := h.config.Update(c.Request.Context(), req)
	if err != nil {
		if isCaasmConfigValidationError(err) {
			response.Error(c, http.StatusBadRequest, err.Error())
			return
		}
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	resolved, err := h.config.ResolveEffectiveThrottling(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, toCaasmConfigResponseDTO(cfg.Data, resolved))
}

func isCaasmConfigValidationError(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(err.Error(), "config validation failed:")
}

func (h *Handler) UpdateTaggingRules(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	var rules []caasmmodel.TaggingRule
	if err := c.ShouldBindJSON(&rules); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid tagging rules payload")
		return
	}
	cfg, err := h.config.Get(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	if cfg.Data == nil {
		cfg.Data = coremodel.JSONB{}
	}
	cfg.Data["tagging_rules"] = rules
	updated, err := h.config.Update(c.Request.Context(), cfg.Data)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, updated.Data)
}

func (h *Handler) UpdatePortCatalog(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	var catalog caasmmodel.PortCatalog
	if err := c.ShouldBindJSON(&catalog); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid port catalog payload")
		return
	}
	cfg, err := h.config.Get(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	if cfg.Data == nil {
		cfg.Data = coremodel.JSONB{}
	}
	cfg.Data["port_catalog"] = catalog
	updated, err := h.config.Update(c.Request.Context(), cfg.Data)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, updated.Data)
}

func (h *Handler) UpdateEphemeralPolicy(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	var policy caasmmodel.EphemeralAssetPolicy
	if err := c.ShouldBindJSON(&policy); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid ephemeral policy payload")
		return
	}
	cfg, err := h.config.Get(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	if cfg.Data == nil {
		cfg.Data = coremodel.JSONB{}
	}
	cfg.Data["ephemeral_asset_policy"] = policy
	updated, err := h.config.Update(c.Request.Context(), cfg.Data)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, updated.Data)
}

func (h *Handler) UpdateDiscoveryIntervals(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	var intervals caasmmodel.DiscoveryIntervals
	if err := c.ShouldBindJSON(&intervals); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid discovery intervals payload")
		return
	}
	cfg, err := h.config.Get(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	if cfg.Data == nil {
		cfg.Data = coremodel.JSONB{}
	}
	cfg.Data["discovery_intervals"] = intervals
	updated, err := h.config.Update(c.Request.Context(), cfg.Data)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, updated.Data)
}

func (h *Handler) UpdateSyncSettings(c *gin.Context) {
	if !requireCAASMManagePermission(c) {
		return
	}
	if h.config == nil {
		response.Error(c, http.StatusInternalServerError, "config service not available")
		return
	}
	var payload struct {
		Mode                    string `json:"mode"`
		LambdaFunctionName      string `json:"lambda_function_name,omitempty"`
		ExecutionTimeoutMinutes int    `json:"execution_timeout_minutes,omitempty"`
		RunningTimeoutMinutes   int    `json:"running_timeout_minutes,omitempty"`
		QueuedTimeoutMinutes    int    `json:"queued_timeout_minutes,omitempty"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid sync settings payload")
		return
	}
	cfg, err := h.config.Get(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	if cfg.Data == nil {
		cfg.Data = coremodel.JSONB{}
	}
	settings, err := h.config.ResolveSyncSettings(c.Request.Context())
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	settings.Mode = payload.Mode
	settings.LambdaFunctionName = strings.TrimSpace(payload.LambdaFunctionName)
	if payload.ExecutionTimeoutMinutes > 0 {
		settings.ExecutionTimeoutMinutes = payload.ExecutionTimeoutMinutes
	}
	if payload.RunningTimeoutMinutes > 0 {
		settings.RunningTimeoutMinutes = payload.RunningTimeoutMinutes
	}
	if payload.QueuedTimeoutMinutes > 0 {
		settings.QueuedTimeoutMinutes = payload.QueuedTimeoutMinutes
	}
	cfg.Data["sync_settings"] = settings
	updated, err := h.config.Update(c.Request.Context(), cfg.Data)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}
	response.Success(c, updated.Data)
}

func requireCAASMManagePermission(c *gin.Context) bool {
	claimsValue, exists := c.Get("current_user")
	if !exists {
		response.Error(c, http.StatusUnauthorized, "not authenticated")
		c.Abort()
		return false
	}
	claims, ok := claimsValue.(*jwtpkg.Claims)
	if !ok {
		response.Error(c, http.StatusUnauthorized, "invalid session")
		c.Abort()
		return false
	}
	if slices.Contains(claims.Roles, "admin") || slices.Contains(claims.Permissions, "caasm:manage_assets") {
		return true
	}
	response.Error(c, http.StatusForbidden, "insufficient permissions")
	c.Abort()
	return false
}

func currentUserID(c *gin.Context) *uint64 {
	claimsValue, exists := c.Get("current_user")
	if !exists {
		return nil
	}
	claims, ok := claimsValue.(*jwtpkg.Claims)
	if !ok {
		return nil
	}
	id := claims.UserID
	if id == 0 {
		return nil
	}
	return &id
}

func currentActorName(c *gin.Context) string {
	claimsValue, exists := c.Get("current_user")
	if !exists {
		return "System"
	}
	if claims, ok := claimsValue.(*jwtpkg.Claims); ok && claims != nil {
		if claims.Email != "" {
			return claims.Email
		}
	}
	return "System"
}

func caasmAuthorizationStatus(err error) int {
	if err == nil {
		return http.StatusOK
	}
	switch {
	case strings.Contains(err.Error(), "not authorized"),
		strings.Contains(err.Error(), "actor required"):
		return http.StatusForbidden
	default:
		return http.StatusInternalServerError
	}
}

func toJSONB(data map[string]interface{}) coremodel.JSONB {
	if data == nil {
		return coremodel.JSONB{}
	}
	return coremodel.JSONB(data)
}

// logAudit writes an audit entry for CAASM mutations.
func (h *Handler) logAudit(c *gin.Context, action, resourceID string, before interface{}, after interface{}) {
	if h.audit == nil {
		return
	}
	claims := currentUserID(c)
	entry := auditservice.Entry{
		Action:       action,
		ResourceType: "caasm",
		ResourceID:   resourceID,
		Metadata: coremodel.JSONB{
			"path": c.Request.URL.Path,
			"verb": c.Request.Method,
		},
		BeforeState: toJSONBInterface(before),
		AfterState:  toJSONBInterface(after),
		IPAddress:   c.ClientIP(),
		UserAgent:   c.GetHeader("User-Agent"),
	}
	if claims != nil {
		entry.UserID = claims
	}
	h.audit.Log(c.Request.Context(), entry)
}

func toJSONBInterface(v interface{}) coremodel.JSONB {
	if v == nil {
		return coremodel.JSONB{}
	}
	switch t := v.(type) {
	case coremodel.JSONB:
		return t
	case map[string]interface{}:
		return coremodel.JSONB(t)
	default:
		// best-effort marshal to map
		var m map[string]interface{}
		bytes, err := json.Marshal(v)
		if err == nil {
			_ = json.Unmarshal(bytes, &m)
			if m != nil {
				return coremodel.JSONB(m)
			}
		}
		return coremodel.JSONB{}
	}
}

func (h *Handler) IngestData(c *gin.Context) {
	if !h.validateIngestAPIKey(c) {
		return
	}

	var payload caasmmodel.IngestPayload
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid payload")
		return
	}

	// TODO: Add authentication check (e.g., API Key)

	if err := h.accounts.IngestSyncData(c.Request.Context(), &payload); err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	h.invalidateAssetListCaches(c.Request.Context())
	response.Success(c, gin.H{"success": true})
}

func (h *Handler) invalidateAssetListCaches(ctx context.Context) {
	if h.cache == nil {
		return
	}
	_ = h.cache.InvalidateByPattern(ctx, "api:v1:caasm:assets:list:*")
	_ = h.cache.InvalidateByPattern(ctx, "api:v1:caasm:domains:list:*")
	_ = h.cache.InvalidateByPattern(ctx, "api:v1:caasm:domains:stats:*")
	_ = h.cache.InvalidateByPattern(ctx, "api:v1:caasm:domains:registrars:*")
	_ = h.cache.InvalidateByPattern(ctx, "api:v1:caasm:dns-records:list:*")
}

func currentCAASMCacheUserID(c *gin.Context) uint64 {
	if id := currentUserID(c); id != nil {
		return *id
	}
	return 0
}

func (h *Handler) serveCachedResponse(c *gin.Context, cacheKey string, ttl time.Duration) bool {
	if h.cache == nil || cacheKey == "" {
		return false
	}
	cached, err := h.cache.Get(c.Request.Context(), cacheKey)
	if err != nil || cached == "" {
		return false
	}
	splitAt := strings.IndexByte(cached, '\n')
	if splitAt <= 0 {
		return false
	}
	etag := cached[:splitAt]
	body := cached[splitAt+1:]
	c.Header("ETag", etag)
	c.Header("Cache-Control", "private, max-age="+strconv.FormatInt(int64(ttl/time.Second), 10))
	if httpcache.IfNoneMatchMatches(c.GetHeader("If-None-Match"), etag) {
		c.Status(http.StatusNotModified)
		return true
	}
	c.Data(http.StatusOK, "application/json", []byte(body))
	return true
}

func (h *Handler) writeCachedResponse(c *gin.Context, cacheKey string, ttl time.Duration, etag string, data interface{}) {
	c.Header("ETag", etag)
	c.Header("Cache-Control", "private, max-age="+strconv.FormatInt(int64(ttl/time.Second), 10))
	if httpcache.IfNoneMatchMatches(c.GetHeader("If-None-Match"), etag) {
		c.Status(http.StatusNotModified)
		return
	}
	apiResp := response.APIResponse{
		Code:      0,
		Message:   "success",
		Data:      data,
		Timestamp: time.Now().Unix(),
	}
	encoded, err := json.Marshal(apiResp)
	if err != nil {
		response.Error(c, http.StatusInternalServerError, "failed to encode response")
		return
	}
	if cacheKey != "" && h.cache != nil {
		_ = h.cache.Set(c.Request.Context(), cacheKey, etag+"\n"+string(encoded), ttl)
	}
	c.Data(http.StatusOK, "application/json", encoded)
}

func (h *Handler) StartSyncJob(c *gin.Context) {
	if !h.validateIngestAPIKey(c) {
		return
	}

	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}

	// TODO: Add authentication check

	if err := h.jobs.Start(c.Request.Context(), id); err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	response.Success(c, gin.H{"success": true})
}

func (h *Handler) ReportSyncJobEvent(c *gin.Context) {
	if !h.validateIngestAPIKey(c) {
		return
	}

	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}

	var payload struct {
		Type     string                 `json:"type" binding:"required"`
		Message  string                 `json:"message" binding:"required"`
		Metadata map[string]interface{} `json:"metadata"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid payload")
		return
	}

	// TODO: Add authentication check

	if err := h.jobs.ReportEvent(c.Request.Context(), id, payload.Type, payload.Message, payload.Metadata); err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	response.Success(c, gin.H{"success": true})
}

// validateIngestAPIKey enforces a simple API key auth for ingestion/lifecycle endpoints.
func (h *Handler) validateIngestAPIKey(c *gin.Context) bool {
	expected := strings.TrimSpace(os.Getenv("CAASM_INGEST_API_KEY"))
	if expected == "" {
		response.Error(c, http.StatusUnauthorized, "unauthorized")
		return false
	}
	key := c.GetHeader("X-CAASM-Ingest-Key")
	if key == "" {
		key = c.GetHeader("X-API-Key")
	}
	if key == "" || key != expected {
		response.Error(c, http.StatusUnauthorized, "unauthorized")
		return false
	}
	return true
}

func (h *Handler) FailSyncJob(c *gin.Context) {
	if !h.validateIngestAPIKey(c) {
		return
	}

	id, err := strconv.ParseUint(c.Param("id"), 10, 64)
	if err != nil || id == 0 {
		response.Error(c, http.StatusBadRequest, "invalid id")
		return
	}

	var payload struct {
		ErrorMessage string `json:"error_message" binding:"required"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid payload")
		return
	}

	if err := h.jobs.Fail(c.Request.Context(), id, payload.ErrorMessage); err != nil {
		response.Error(c, http.StatusInternalServerError, err.Error())
		return
	}

	response.Success(c, gin.H{"success": true})
}
