package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"regexp"
	"strconv"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/request"
	"github.com/pingcap/dbaas/billing2/utils/user"
)

const (
	defaultPayingLimit = 20
	maxPayingLimit     = 100
)

var payingMonthPattern = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`)

// CreatePayingRequest is the request body for creating a Stripe invoice paying task.
type CreatePayingRequest struct {
	Month string `json:"month" binding:"required" example:"2026-06"`
	Force bool   `json:"force,omitempty" example:"false"`
}

// StripePayingResponse is the public representation of a Stripe invoice paying task.
type StripePayingResponse struct {
	PayingID        uint        `json:"paying_id"`
	Month           string      `json:"month"`
	Status          string      `json:"status"`
	CreatedBy       string      `json:"created_by,omitempty"`
	CreatedAt       time.Time   `json:"created_at"`
	PickedUpAt      *time.Time  `json:"picked_up_at,omitempty"`
	CompletedAt     *time.Time  `json:"completed_at,omitempty"`
	DurationSeconds int64       `json:"duration_seconds,omitempty"`
	Result          interface{} `json:"result"`
	ErrorMessage    string      `json:"error_message,omitempty"`
}

// StripePayingDetailResponse is the public representation returned by the detail endpoint.
type StripePayingDetailResponse = StripePayingResponse

// StripePayingDetailResult wraps a full paying task for the detail endpoint.
type StripePayingDetailResult struct {
	Paying StripePayingDetailResponse `json:"paying"`
}

// StripePayingErrorResponse is the standard billing API error response.
type StripePayingErrorResponse struct {
	Error   string `json:"error"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

// StripePayingConflictResponse reports the active task that blocked creation.
type StripePayingConflictResponse struct {
	Error          string               `json:"error"`
	Message        string               `json:"message"`
	ExistingPaying StripePayingResponse `json:"existing_paying"`
}

// StripePayingCarryoverSummary contains the carryover pool totals shown on the dashboard.
type StripePayingCarryoverSummary struct {
	TotalCount            int64   `json:"total_count"`
	TotalAmount           string  `json:"total_amount"`
	OldestFirstCarryMonth *string `json:"oldest_first_carry_month"`
}

// StripePayingListResponse is the dashboard response for recent paying tasks.
type StripePayingListResponse struct {
	Payings          []StripePayingResponse       `json:"payings"`
	CarryoverSummary StripePayingCarryoverSummary `json:"carryover_summary"`
}

var payingModel = &payment.PayingModel{}
var carryoverModel = &payment.CarryoverModel{}

func validatePayingMonth(month string) error {
	if !payingMonthPattern.MatchString(month) {
		return billing_errors.NewInvalidInputError(fmt.Errorf("month must use yyyy-mm format"))
	}
	return nil
}

func payingCreatedBy() string {
	return fmt.Sprintf("user:%d", user.GetCurrentUserID())
}

func payingResponse(paying *entity.StripeInvoicePaying) StripePayingResponse {
	response := StripePayingResponse{
		PayingID:     paying.ID,
		Month:        paying.Month,
		Status:       paying.Status,
		CreatedBy:    paying.CreatedBy,
		CreatedAt:    paying.CreatedAt,
		PickedUpAt:   paying.PickedUpAt,
		CompletedAt:  paying.CompletedAt,
		ErrorMessage: paying.ErrorMessage,
	}
	if paying.PickedUpAt != nil {
		end := paying.CompletedAt
		if end == nil {
			now := time.Now()
			end = &now
		}
		if !end.Before(*paying.PickedUpAt) {
			response.DurationSeconds = int64(end.Sub(*paying.PickedUpAt).Seconds())
		}
	}
	if paying.Result != nil && *paying.Result != "" {
		var result interface{}
		if err := json.Unmarshal([]byte(*paying.Result), &result); err == nil {
			response.Result = result
		} else {
			response.Result = *paying.Result
		}
	}
	return response
}

func activePayingResponse(month string, paying *entity.StripeInvoicePaying) StripePayingConflictResponse {
	return StripePayingConflictResponse{
		Error:          "paying_already_active",
		Message:        fmt.Sprintf("There is already an active paying for month %s. Wait for it to complete before creating a new one.", month),
		ExistingPaying: payingResponse(paying),
	}
}

func activePayingOrNil(month string) (*entity.StripeInvoicePaying, error) {
	paying, err := payingModel.ListActivePayingByMonth(month)
	if err != nil && !errors.Is(err, billing_errors.ObjectNotExistsError) {
		return nil, err
	}
	return paying, nil
}

// CreatePaying handles POST /paying.
// @Summary Create a Stripe invoice paying task
// @Tags StripePaying
// @Description Creates a persisted Stripe invoice paying task for the requested month.
// @Accept json
// @Produce json
// @Param request body CreatePayingRequest true "Paying request"
// @Success 201 {object} StripePayingResponse
// @Success 400 {object} StripePayingErrorResponse
// @Success 409 {object} StripePayingConflictResponse
// @Router /paying [post]
func (s *API) CreatePaying(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	var req CreatePayingRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if err := validatePayingMonth(req.Month); err != nil {
		return nil, err
	}
	if req.Force && !request.SourceFromOps(c.Request) {
		return nil, billing_errors.NewForbiddenError(errors.New("force paying is restricted to Ops"))
	}

	if !req.Force {
		existing, err := activePayingOrNil(req.Month)
		if err != nil {
			return nil, err
		}
		if existing != nil {
			c.JSON(http.StatusConflict, activePayingResponse(req.Month, existing))
			return nil, nil
		}
	}

	paying, err := payingModel.CreatePaying(req.Month, payingCreatedBy(), req.Force)
	if err != nil {
		if errors.Is(err, billing_errors.PayingAlreadyActiveError) {
			existing, lookupErr := activePayingOrNil(req.Month)
			if lookupErr != nil {
				return nil, lookupErr
			}
			if existing != nil {
				c.JSON(http.StatusConflict, activePayingResponse(req.Month, existing))
				return nil, nil
			}
		}
		return nil, err
	}

	c.JSON(http.StatusCreated, payingResponse(paying))
	return nil, nil
}

// ListPayings handles GET /paying.
// @Summary List recent Stripe invoice paying tasks
// @Tags StripePaying
// @Description Returns recent paying tasks and the carryover dashboard summary.
// @Produce json
// @Param paying_limit query int false "Number of payings to return (maximum 100)" default(20)
// @Success 200 {object} StripePayingListResponse
// @Router /paying [get]
func (s *API) ListPayings(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	limit := defaultPayingLimit
	if value := c.Query("paying_limit"); value != "" {
		parsed, err := strconv.Atoi(value)
		if err != nil || parsed <= 0 {
			return nil, billing_errors.NewInvalidInputError(errors.New("paying_limit must be a positive integer"))
		}
		limit = parsed
	}
	if limit > maxPayingLimit {
		limit = maxPayingLimit
	}

	payings, err := payingModel.ListRecentPayings(limit)
	if err != nil {
		return nil, err
	}
	// NOTE: since the fix/stripe-paying-runner-bugs refactor,
	// billing2_invoices.status is the single source of truth for carryover
	// and the paying worker no longer writes stripe_invoice_push_carryover.
	// CarryoverModel.Summary() still reads the pool table, so this summary
	// shows stale data (a write-through cache nobody updates). It is kept
	// as-is for now; a follow-up should switch it to a direct
	// billing2_invoices query (count + sum of below_threshold_carry rows).
	summary, err := carryoverModel.Summary()
	if err != nil {
		return nil, err
	}

	result := StripePayingListResponse{
		Payings: make([]StripePayingResponse, 0, len(payings)),
		CarryoverSummary: StripePayingCarryoverSummary{
			TotalCount:  summary.TotalCount,
			TotalAmount: summary.TotalAmount.StringFixed(2),
		},
	}
	if summary.OldestFirstCarryMonth != nil {
		month := summary.OldestFirstCarryMonth.Format("2006-01")
		result.CarryoverSummary.OldestFirstCarryMonth = &month
	}
	for _, paying := range payings {
		result.Payings = append(result.Payings, payingResponse(paying))
	}
	return result, nil
}

// GetPaying handles GET /paying/{id}.
// @Summary Get a Stripe invoice paying task
// @Tags StripePaying
// @Description Returns the full result of one Stripe invoice paying task.
// @Produce json
// @Param id path uint true "Paying ID"
// @Success 200 {object} StripePayingDetailResult
// @Success 404 {object} StripePayingErrorResponse
// @Router /paying/{id} [get]
func (s *API) GetPaying(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	id, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil || id == 0 {
		return nil, billing_errors.NewInvalidInputError(errors.New("paying id must be a positive integer"))
	}
	paying, err := payingModel.GetPaying(uint(id))
	if err != nil {
		return nil, err
	}
	return StripePayingDetailResult{Paying: payingResponse(paying)}, nil
}

// stripePayingHandler is the route adapter for endpoints that may write a
// non-200 response themselves. The shared adapter always writes HTTP 200 after
// the handler returns, so it must not write a second response in that case.
func stripePayingHandler(handler ginutils.HandlerFunc) gin.HandlerFunc {
	return func(c *gin.Context) {
		result, err := handler(c)
		if err != nil {
			ginutils.ReturnError(c, err)
			return
		}
		if c.Writer.Written() {
			return
		}
		c.JSON(http.StatusOK, ginutils.FilterByQueryMiddlwWare(c, result))
	}
}

// InitializeStripeInvoicePayingRouter registers the top-level Stripe invoice paying API.
func InitializeStripeInvoicePayingRouter(router ginutils.MyRoute) {
	s := &API{}
	router.Handle(http.MethodPost, "/paying", stripePayingHandler(s.CreatePaying))
	router.Handle(http.MethodGet, "/paying", stripePayingHandler(s.ListPayings))
	router.Handle(http.MethodGet, "/paying/:id", stripePayingHandler(s.GetPaying))
}
