package handler

import (
	"bytes"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"security-center-backend/internal/modules/scan/service"
	"security-center-backend/internal/pkg/response"
)

const maxCallbackBodyBytes = 4 << 20

type Handler struct {
	scans *service.Service
}

func New(scans *service.Service) *Handler {
	return &Handler{scans: scans}
}

func (h *Handler) RegisterViewRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	scans := rg.Group("/scans")
	scans.GET("/port-exposures", h.ListPortExposures)
	scans.GET("/stats", h.GetStats)
	scans.GET("/campaigns", h.ListCampaigns)
	scans.GET("/campaigns/:id", h.GetCampaign)
	scans.GET("/campaigns/:id/batches", h.ListCampaignBatches)
	scans.GET("/batches", h.ListBatches)
	scans.GET("/batches/:id", h.GetBatch)
	scans.GET("/batches/:id/targets", h.ListTargets)
	scans.GET("/batches/:id/observations", h.ListObservations)
	scans.GET("/batches/:id/callback-receipts", h.ListCallbackReceipts)
}

func (h *Handler) RegisterCreateRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	scans := rg.Group("/scans")
	scans.POST("/campaigns", h.CreateCampaign)
	scans.POST("/batches", h.CreateBatch)
}

func (h *Handler) RegisterManageRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	scans := rg.Group("/scans")
	scans.POST("/campaigns/:id/sync", h.SyncCampaign)
	scans.POST("/campaigns/:id/cancel", h.CancelCampaign)
	scans.POST("/batches/:id/sync", h.SyncBatch)
	scans.POST("/batches/:id/cancel", h.CancelBatch)
}

func (h *Handler) RegisterCallbackRoutes(rg *gin.RouterGroup) {
	if rg == nil {
		return
	}
	rg.POST("/scans/callback", h.Callback)
}

func (h *Handler) ListPortExposures(c *gin.Context) {
	result, err := h.scans.ListPortExposures(c.Request.Context(), service.ListPortExposuresFilter{
		Pagination:  paginationFromQuery(c),
		Result:      c.Query("result"),
		Query:       c.Query("q"),
		AssetID:     optionalUint64(c.Query("asset_id")),
		PublicIP:    c.Query("public_ip"),
		Port:        optionalInt(c.Query("port")),
		Environment: c.Query("environment"),
		Owner:       c.Query("owner"),
		Risk:        c.Query("risk"),
		QuickFilter: c.Query("quick_filter"),
		Service:     c.Query("service"),
		SortBy:      c.Query("sort_by"),
		SortOrder:   c.Query("sort_order"),
	})
	writeEnvelope(c, result, err)
}

func (h *Handler) ListBatches(c *gin.Context) {
	result, err := h.scans.ListBatches(c.Request.Context(), service.ListBatchesFilter{
		Pagination: paginationFromQuery(c),
		Status:     c.Query("status"),
		Query:      c.Query("q"),
		CreatedBy:  optionalUint64(c.Query("created_by")),
		From:       optionalTime(c.Query("from")),
		To:         optionalTime(c.Query("to")),
		Result:     c.Query("result"),
		AssetID:    optionalUint64(c.Query("asset_id")),
		PublicIP:   c.Query("public_ip"),
		CampaignID: optionalUint64(c.Query("campaign_id")),
	})
	writeEnvelope(c, result, err)
}

func (h *Handler) ListCampaigns(c *gin.Context) {
	result, err := h.scans.ListCampaigns(c.Request.Context(), service.ListCampaignsFilter{
		Pagination: paginationFromQuery(c),
		Status:     c.Query("status"),
		Query:      c.Query("q"),
		CreatedBy:  optionalUint64(c.Query("created_by")),
		From:       optionalTime(c.Query("from")),
		To:         optionalTime(c.Query("to")),
	})
	writeEnvelope(c, result, err)
}

func (h *Handler) GetCampaign(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid campaign id")
		return
	}
	result, err := h.scans.GetCampaign(c.Request.Context(), id)
	writeEnvelope(c, result, err)
}

func (h *Handler) ListCampaignBatches(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid campaign id")
		return
	}
	result, err := h.scans.ListCampaignBatches(c.Request.Context(), id, service.ListBatchesFilter{
		Pagination: paginationFromQuery(c),
		Status:     c.Query("status"),
		Query:      c.Query("q"),
		CreatedBy:  optionalUint64(c.Query("created_by")),
		From:       optionalTime(c.Query("from")),
		To:         optionalTime(c.Query("to")),
		Result:     c.Query("result"),
		AssetID:    optionalUint64(c.Query("asset_id")),
		PublicIP:   c.Query("public_ip"),
	})
	writeEnvelope(c, result, err)
}

func (h *Handler) GetBatch(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid batch id")
		return
	}
	result, err := h.scans.GetBatch(c.Request.Context(), id)
	writeEnvelope(c, result, err)
}

func (h *Handler) ListTargets(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid batch id")
		return
	}
	result, err := h.scans.ListTargets(c.Request.Context(), id, service.ListTargetsFilter{
		Pagination:   paginationFromQuery(c),
		TargetStatus: c.Query("target_status"),
		PublicIP:     c.Query("public_ip"),
		CAASMAssetID: optionalUint64(c.Query("caasm_asset_id")),
		CAASMPortID:  optionalUint64(c.Query("caasm_port_id")),
	})
	writeEnvelope(c, result, err)
}

func (h *Handler) ListObservations(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid batch id")
		return
	}
	result, err := h.scans.ListObservations(c.Request.Context(), id, paginationFromQuery(c))
	writeEnvelope(c, result, err)
}

func (h *Handler) ListCallbackReceipts(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid batch id")
		return
	}
	result, err := h.scans.ListCallbackReceipts(c.Request.Context(), id, paginationFromQuery(c))
	writeEnvelope(c, result, err)
}

func (h *Handler) GetStats(c *gin.Context) {
	result, err := h.scans.GetStats(c.Request.Context())
	writeEnvelope(c, result, err)
}

func (h *Handler) CreateBatch(c *gin.Context) {
	var req service.CreateBatchRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid payload")
		return
	}
	result, err := h.scans.CreateBatch(c.Request.Context(), req)
	writeEnvelope(c, result, err)
}

func (h *Handler) CreateCampaign(c *gin.Context) {
	var req service.CreateCampaignRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, http.StatusBadRequest, "invalid payload")
		return
	}
	result, err := h.scans.CreateCampaign(c.Request.Context(), req)
	writeEnvelope(c, result, err)
}

func (h *Handler) SyncBatch(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid batch id")
		return
	}
	result, err := h.scans.SyncBatch(c.Request.Context(), id)
	writeEnvelope(c, result, err)
}

func (h *Handler) SyncCampaign(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid campaign id")
		return
	}
	result, err := h.scans.SyncCampaign(c.Request.Context(), id)
	writeEnvelope(c, result, err)
}

func (h *Handler) CancelBatch(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid batch id")
		return
	}
	result, err := h.scans.CancelBatch(c.Request.Context(), id)
	writeEnvelope(c, result, err)
}

func (h *Handler) CancelCampaign(c *gin.Context) {
	id, err := pathUint64(c, "id")
	if err != nil {
		response.Error(c, http.StatusBadRequest, "invalid campaign id")
		return
	}
	result, err := h.scans.CancelCampaign(c.Request.Context(), id)
	writeEnvelope(c, result, err)
}

func (h *Handler) Callback(c *gin.Context) {
	if h.scans == nil {
		writeSimpleJSON(c, http.StatusInternalServerError, map[string]string{"error": "scan service not configured"})
		return
	}
	if !validBearerToken(c.GetHeader("Authorization"), h.scans.CallbackToken()) {
		writeSimpleJSON(c, http.StatusUnauthorized, map[string]string{"error": "invalid callback token"})
		return
	}
	idempotencyKey := strings.TrimSpace(c.GetHeader("Idempotency-Key"))
	requestHeaderID := strings.TrimSpace(c.GetHeader("X-Request-ID"))
	headerHash := strings.ToLower(strings.TrimSpace(c.GetHeader("X-Payload-SHA256")))
	if idempotencyKey == "" || requestHeaderID == "" || headerHash == "" {
		writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": "missing required callback headers"})
		return
	}
	raw, err := io.ReadAll(io.LimitReader(c.Request.Body, maxCallbackBodyBytes+1))
	if err != nil {
		writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": "failed to read request body"})
		return
	}
	if len(raw) > maxCallbackBodyBytes {
		writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": "callback body too large"})
		return
	}
	computed := sha256.Sum256(raw)
	computedHash := hex.EncodeToString(computed[:])
	if subtle.ConstantTimeCompare([]byte(strings.ToLower(headerHash)), []byte(computedHash)) != 1 {
		writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": "payload hash mismatch"})
		return
	}
	var payload service.CallbackPayload
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&payload); err != nil {
		writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": "malformed callback payload"})
		return
	}
	if err := validateCallbackPayload(payload); err != nil {
		writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": err.Error()})
		return
	}
	result, err := h.scans.ApplyCallback(c.Request.Context(), service.CallbackEnvelope{
		IdempotencyKey:      idempotencyKey,
		RequestHeaderID:     requestHeaderID,
		HeaderPayloadSHA256: headerHash,
		PayloadSHA256:       computedHash,
		RawPayload:          raw,
		Payload:             payload,
	})
	if err != nil {
		if service.IsCallbackDuplicate(err) {
			writeSimpleJSON(c, http.StatusOK, map[string]string{"status": service.CallbackStatusDuplicate})
			return
		}
		if service.IsCallbackConflict(err) {
			writeSimpleJSON(c, http.StatusConflict, map[string]string{"error": "idempotency key payload conflict"})
			return
		}
		if service.IsUnknownBatch(err) {
			writeSimpleJSON(c, http.StatusNotFound, map[string]string{"error": "scan batch not found"})
			return
		}
		if service.IsInvalidRequest(err) {
			writeSimpleJSON(c, http.StatusBadRequest, map[string]string{"error": err.Error()})
			return
		}
		writeSimpleJSON(c, http.StatusInternalServerError, map[string]string{"error": "failed to persist callback"})
		return
	}
	status := service.CallbackStatusAccepted
	if result != nil && result.Status != "" {
		status = result.Status
	}
	writeSimpleJSON(c, http.StatusOK, map[string]string{"status": status})
}

func validateCallbackPayload(payload service.CallbackPayload) error {
	if strings.TrimSpace(payload.BatchID) == "" && strings.TrimSpace(payload.RequestID) == "" {
		return errors.New("callback must include batch_id or request_id")
	}
	if strings.TrimSpace(payload.Status) == "" {
		return errors.New("callback status is required")
	}
	if !service.IsSupportedCallbackStatus(payload.Status) {
		return errors.New("callback status is unsupported")
	}
	for _, obs := range payload.Observations {
		if strings.TrimSpace(obs.Result) == "" {
			return errors.New("observation result is required")
		}
		if strings.TrimSpace(obs.PublicIP) == "" {
			return errors.New("observation public_ip is required")
		}
		if obs.Port < 1 || obs.Port > 65535 {
			return errors.New("observation port is invalid")
		}
	}
	return nil
}

func writeEnvelope(c *gin.Context, data any, err error) {
	if err == nil {
		response.Success(c, data)
		return
	}
	if service.IsInvalidRequest(err) {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	if service.IsUnknownBatch(err) {
		response.Error(c, http.StatusNotFound, err.Error())
		return
	}
	if service.IsUnknownCampaign(err) {
		response.Error(c, http.StatusNotFound, err.Error())
		return
	}
	if errors.Is(err, service.ErrNoAcceptedTargets) {
		response.Error(c, http.StatusBadRequest, err.Error())
		return
	}
	if errors.Is(err, service.ErrMissingDependency) {
		response.Error(c, http.StatusServiceUnavailable, err.Error())
		return
	}
	if service.IsScannerConflict(err) {
		response.Error(c, http.StatusConflict, err.Error())
		return
	}
	if service.IsScannerUnavailable(err) {
		response.Error(c, http.StatusServiceUnavailable, err.Error())
		return
	}
	response.Error(c, http.StatusInternalServerError, err.Error())
}

func writeSimpleJSON(c *gin.Context, status int, body map[string]string) {
	c.JSON(status, body)
}

func validBearerToken(header string, expected string) bool {
	expected = strings.TrimSpace(expected)
	if expected == "" {
		return false
	}
	const prefix = "Bearer "
	if !strings.HasPrefix(header, prefix) {
		return false
	}
	actual := strings.TrimSpace(strings.TrimPrefix(header, prefix))
	actualHash := sha256.Sum256([]byte(actual))
	expectedHash := sha256.Sum256([]byte(expected))
	return subtle.ConstantTimeCompare(actualHash[:], expectedHash[:]) == 1
}

func paginationFromQuery(c *gin.Context) service.Pagination {
	return service.Pagination{
		Page:     intFromQuery(c, "page", 1),
		PageSize: intFromQuery(c, "page_size", 20),
	}
}

func intFromQuery(c *gin.Context, key string, fallback int) int {
	value := strings.TrimSpace(c.Query(key))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return fallback
	}
	return parsed
}

func pathUint64(c *gin.Context, key string) (uint64, error) {
	value := strings.TrimSpace(c.Param(key))
	parsed, err := strconv.ParseUint(value, 10, 64)
	if err != nil || parsed == 0 {
		return 0, errors.New("invalid id")
	}
	return parsed, nil
}

func optionalUint64(value string) *uint64 {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil
	}
	parsed, err := strconv.ParseUint(value, 10, 64)
	if err != nil || parsed == 0 {
		return nil
	}
	return &parsed
}

func optionalInt(value string) *int {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return nil
	}
	return &parsed
}

func optionalTime(value string) *time.Time {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil
	}
	for _, layout := range []string{time.RFC3339, "2006-01-02"} {
		parsed, err := time.Parse(layout, value)
		if err == nil {
			return &parsed
		}
	}
	return nil
}
