package api

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

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/awshandler"
	"github.com/pingcap/dbaas/billing2/model/payment/reconciliation"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"go.uber.org/zap"
)

type ARTableAPI struct{}

var arTableModel = &reconciliation.ARTableModel{}

const (
	arTableTypeActual    = "actual"
	arTableTypeEstimated = "estimated"

	arTableExportStatusRunning  = "running"
	arTableExportStatusReady    = "ready"
	arTableExportStatusTimeout  = "timeout"
	arTableExportStatusNotFound = "not_found"
)

type arTableExportState struct {
	Key       string
	Status    string
	StartedAt *time.Time
	TimeoutAt *time.Time
}

type ARTableExportHooks struct {
	Now            func() time.Time
	GetMarker      func(string) (*awshandler.ARTableExportMarker, bool, error)
	PutMarker      func(string, *awshandler.ARTableExportMarker) error
	DeleteMarker   func(string) error
	GetLastMod     func(string) (*time.Time, bool, error)
	GetPresignURL  func(string) (string, error)
	StartExportJob func(time.Time, string, string, string, *awshandler.ARTableExportMarker)
}

func newDefaultARTableExportHooks() ARTableExportHooks {
	return ARTableExportHooks{
		Now:           timeutils.GetNow,
		GetMarker:     awshandler.GetARTableRunningMarker,
		PutMarker:     awshandler.PutARTableRunningMarker,
		DeleteMarker:  awshandler.DeleteARTableRunningMarker,
		GetLastMod:    awshandler.GetARTableObjectLastModified,
		GetPresignURL: awshandler.GetARTablePresignURL,
		StartExportJob: func(date time.Time, arType string, finalKey string, markerKey string, marker *awshandler.ARTableExportMarker) {
			go runARTableExport(date, arType, finalKey, markerKey, marker)
		},
	}
}

var currentARTableExportHooks ARTableExportHooks

func init() {
	currentARTableExportHooks = newDefaultARTableExportHooks()
}

func SetARTableExportHooksForTest(hooks *ARTableExportHooks) func() {
	previous := currentARTableExportHooks
	currentARTableExportHooks = newDefaultARTableExportHooks()
	if hooks != nil {
		if hooks.Now != nil {
			currentARTableExportHooks.Now = hooks.Now
		}
		if hooks.GetMarker != nil {
			currentARTableExportHooks.GetMarker = hooks.GetMarker
		}
		if hooks.PutMarker != nil {
			currentARTableExportHooks.PutMarker = hooks.PutMarker
		}
		if hooks.DeleteMarker != nil {
			currentARTableExportHooks.DeleteMarker = hooks.DeleteMarker
		}
		if hooks.GetLastMod != nil {
			currentARTableExportHooks.GetLastMod = hooks.GetLastMod
		}
		if hooks.GetPresignURL != nil {
			currentARTableExportHooks.GetPresignURL = hooks.GetPresignURL
		}
		if hooks.StartExportJob != nil {
			currentARTableExportHooks.StartExportJob = hooks.StartExportJob
		}
	}
	return func() {
		currentARTableExportHooks = previous
	}
}

func getARTableIDFromURI(c *gin.Context) (uint, error) {
	idStr := c.Param("id")
	id, err := strconv.ParseUint(idStr, 10, 64)
	if err != nil {
		return 0, billing_errors.NewInvalidInputError(err)
	}
	return uint(id), nil
}

func validateARTableExportDate(year, month int) error {
	if month < 1 || month > 12 {
		return billing_errors.NewInvalidInputError(fmt.Errorf("month must be between 1 and 12"))
	}
	nowYear := timeutils.GetNow().Year()
	if year < 2020 || year > nowYear+1 {
		return billing_errors.NewInvalidInputError(fmt.Errorf("year must be between 2020 and %d", nowYear+1))
	}
	return nil
}

func buildARTableExportState(finalKey string, marker *awshandler.ARTableExportMarker, now time.Time, finalLastModified *time.Time, finalExists bool) *arTableExportState {
	state := &arTableExportState{Key: finalKey, Status: arTableExportStatusNotFound}
	if marker != nil {
		state.StartedAt = &marker.StartedAt
		state.TimeoutAt = &marker.TimeoutAt
		if finalExists && finalLastModified != nil && !finalLastModified.Before(marker.StartedAt) {
			state.Status = arTableExportStatusReady
			return state
		}
		if !marker.TimeoutAt.Before(now) {
			state.Status = arTableExportStatusRunning
			return state
		}
		state.Status = arTableExportStatusTimeout
		return state
	}
	if finalExists {
		state.Status = arTableExportStatusReady
	}
	return state
}

func getARTableExportState(date time.Time, arType string) (*arTableExportState, string, error) {
	finalKey := awshandler.BuildS3ARTableKey(date, arType)
	markerKey := awshandler.BuildS3ARTableRunningKey(date, arType)
	marker, exists, err := currentARTableExportHooks.GetMarker(markerKey)
	if err != nil {
		return nil, "", err
	}
	if !exists {
		marker = nil
	}
	lastModified, finalExists, err := currentARTableExportHooks.GetLastMod(finalKey)
	if err != nil {
		return nil, "", err
	}
	return buildARTableExportState(finalKey, marker, currentARTableExportHooks.Now(), lastModified, finalExists), markerKey, nil
}

func formatARTableTime(t *time.Time) string {
	if t == nil {
		return ""
	}
	return t.UTC().Format(time.RFC3339)
}

func toARTableExportStatusResponse(state *arTableExportState) *forms.ARTableExportStatusResponse {
	return &forms.ARTableExportStatusResponse{
		Key:       state.Key,
		Status:    state.Status,
		StartedAt: formatARTableTime(state.StartedAt),
		TimeoutAt: formatARTableTime(state.TimeoutAt),
	}
}

func clearARTableMarkerIfOwned(markerKey, runID string) {
	marker, exists, err := currentARTableExportHooks.GetMarker(markerKey)
	if err != nil {
		log.Error("failed to read ar table marker before cleanup", zap.Error(err), zap.String("key", markerKey))
		return
	}
	if !exists || marker == nil || marker.RunID != runID {
		return
	}
	if err := currentARTableExportHooks.DeleteMarker(markerKey); err != nil {
		log.Error("failed to delete ar table marker", zap.Error(err), zap.String("key", markerKey))
	}
}

func runARTableExport(date time.Time, arType string, finalKey string, markerKey string, marker *awshandler.ARTableExportMarker) {
	var err error
	defer func() {
		clearARTableMarkerIfOwned(markerKey, marker.RunID)
		log.Info("ar table export finished", zap.String("type", arType), zap.String("key", finalKey), zap.Error(err))
	}()

	log.Info("start to generate ar table export", zap.String("type", arType), zap.Time("date", date), zap.String("key", finalKey))

	var rows []*reconciliation.ARTableRow
	if arType == arTableTypeEstimated {
		rows, err = arTableModel.GenerateEstimatedARTable(date)
	} else {
		rows, err = arTableModel.GenerateARInitTable(date)
	}
	if err != nil {
		log.Error("failed to generate ar table", zap.Error(err), zap.String("type", arType))
		return
	}
	if err = arTableModel.SaveARTableToDB(rows, date); err != nil {
		log.Error("failed to save AR table to DB", zap.Error(err), zap.String("type", arType))
		return
	}
	var ret []byte
	ret, err = arTableModel.RenderARTable(rows)
	if err != nil {
		log.Error("failed to render ar table", zap.Error(err), zap.String("type", arType))
		return
	}
	currentMarker, exists, markerErr := currentARTableExportHooks.GetMarker(markerKey)
	if markerErr != nil {
		err = markerErr
		log.Error("failed to re-check ar table marker", zap.Error(err), zap.String("key", markerKey))
		return
	}
	if !exists || currentMarker == nil || currentMarker.RunID != marker.RunID {
		log.Warn("skip uploading stale ar table export result", zap.String("type", arType), zap.String("key", finalKey))
		return
	}
	err = arTableModel.PutARTableResultToS3(finalKey, ret)
	if err != nil {
		log.Error("failed to put ar table to S3", zap.Error(err), zap.String("key", finalKey), zap.String("type", arType))
		return
	}
}

func triggerARTableExport(c *gin.Context, arType string) (ginutils.HandlerFuncResult, error) {
	if !config.Get().GetBool(config.EnablePutARTableToS3) {
		return nil, billing_errors.NewInvalidInputError(errors.New("AR table S3 export is not enabled"))
	}
	query := &struct {
		Year  int `form:"year" binding:"required"`
		Month int `form:"month" binding:"required"`
	}{}
	if err := c.ShouldBindQuery(query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if err := validateARTableExportDate(query.Year, query.Month); err != nil {
		return nil, err
	}
	date := time.Date(query.Year, time.Month(query.Month), 1, 0, 0, 0, 0, time.UTC)
	state, markerKey, err := getARTableExportState(date, arType)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	if state.Status == arTableExportStatusRunning {
		return forms.ARTableTriggerResponse{Key: state.Key, Status: state.Status}, nil
	}
	now := currentARTableExportHooks.Now()
	timeout := time.Duration(config.Get().GetInt(config.ARTableExportTimeoutMinutes)) * time.Minute
	marker := &awshandler.ARTableExportMarker{
		RunID:     uuid.NewString(),
		StartedAt: now,
		TimeoutAt: now.Add(timeout),
		Year:      query.Year,
		Month:     query.Month,
		Type:      arType,
	}
	if err := currentARTableExportHooks.PutMarker(markerKey, marker); err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	currentARTableExportHooks.StartExportJob(date, arType, state.Key, markerKey, marker)
	return forms.ARTableTriggerResponse{Key: state.Key, Status: arTableExportStatusRunning}, nil
}

func (s *ARTableAPI) GetInitARTable(c *gin.Context) {
	query := &struct {
		Year  int `form:"year"`
		Month int `form:"month"`
		Day   int `form:"day"`
	}{}
	if err := c.ShouldBindQuery(query); err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	date := time.Date(query.Year, time.Month(query.Month), 1, 0, 0, 0, 0, time.UTC)
	rows, err := arTableModel.GenerateARInitTable(date)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	if err := arTableModel.SaveARTableToDB(rows, date); err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	ret, err := arTableModel.RenderARTable(rows)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	c.Header("Content-Disposition", "attachment; filename=ARTable_"+date.Format("2006-01-02")+".csv")
	c.Data(http.StatusOK, "application/CSV", ret)
}

func (s *ARTableAPI) InitARTableTrigger(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	return triggerARTableExport(c, arTableTypeActual)
}

func (s *ARTableAPI) InitEstimatedARTableTrigger(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	return triggerARTableExport(c, arTableTypeEstimated)
}

// GetARTableExportStatus godoc
// @Summary Get AR Table export status
// @Description Query async AR table export status by year, month and type(actual|estimated).
// @Schemes
// @Accept json
// @Produce json
// @Param year query int true "year"
// @Param month query int true "month"
// @Param type query string true "export type: actual or estimated"
// @Success 200 {object} forms.ARTableExportStatusResponse
// @Failure 400 {object} billing_errors.BaseError
// @Router /ar_tables/export/status [get]
func (s *ARTableAPI) GetARTableExportStatus(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	if !config.Get().GetBool(config.EnablePutARTableToS3) {
		return nil, billing_errors.NewInvalidInputError(errors.New("AR table S3 export is not enabled"))
	}
	query := &forms.ARTableExportQuery{}
	if err := c.ShouldBindQuery(query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if err := validateARTableExportDate(query.Year, query.Month); err != nil {
		return nil, err
	}
	date := time.Date(query.Year, time.Month(query.Month), 1, 0, 0, 0, 0, time.UTC)
	state, _, err := getARTableExportState(date, query.Type)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return toARTableExportStatusResponse(state), nil
}

// DownloadARTableExport godoc
// @Summary Get AR Table export download URL
// @Description Get a presigned download URL for an async AR table export once it is ready.
// @Schemes
// @Accept json
// @Produce json
// @Param year query int true "year"
// @Param month query int true "month"
// @Param type query string true "export type: actual or estimated"
// @Success 200 {object} forms.ARTableExportDownloadResponse
// @Failure 400 {object} billing_errors.BaseError
// @Failure 404 {object} billing_errors.BaseError
// @Router /ar_tables/export/download [get]
func (s *ARTableAPI) DownloadARTableExport(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	if !config.Get().GetBool(config.EnablePutARTableToS3) {
		return nil, billing_errors.NewInvalidInputError(errors.New("AR table S3 export is not enabled"))
	}
	query := &forms.ARTableExportQuery{}
	if err := c.ShouldBindQuery(query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if err := validateARTableExportDate(query.Year, query.Month); err != nil {
		return nil, err
	}
	date := time.Date(query.Year, time.Month(query.Month), 1, 0, 0, 0, 0, time.UTC)
	state, _, err := getARTableExportState(date, query.Type)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	if state.Status == arTableExportStatusNotFound {
		return nil, billing_errors.NewObjectNotExistsError(fmt.Errorf("AR table export does not exist"))
	}
	if state.Status != arTableExportStatusReady {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("AR table export is %s", state.Status))
	}
	url, err := currentARTableExportHooks.GetPresignURL(state.Key)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return forms.ARTableExportDownloadResponse{Key: state.Key, URL: url}, nil
}

// ListARTables godoc
// @Summary List AR Table Records
// @Description Query AR table records with pagination and optional filters (year, month, is_estimated, tenant_id, payment_type, binded_billing_config_id).
// @Schemes
// @Accept json
// @Produce json
// @Param year query int false "filter by year (used with month, e.g. 2025)"
// @Param month query int false "filter by month (used with year, e.g. 4)"
// @Param is_estimated query bool false "filter by estimated flag"
// @Param tenant_id query int false "filter by tenant ID"
// @Param payment_type query string false "filter by payment type (e.g. stripe, wire_transfer, aws)"
// @Param binded_billing_config_id query int false "filter by binded billing config ID"
// @Param page query int false "page number, default=1, min=1, max=100"
// @Param page_size query int false "page size, default=20, min=1, max=100"
// @Success 200 {object} forms.ARTableListResponse
// @Failure 400 {object} billing_errors.BaseError
// @Router /ar_tables [get]
func (s *ARTableAPI) ListARTables(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	query := &forms.ARTableListQuery{}
	if err := c.ShouldBindQuery(query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if query.Year != 0 && query.Month == 0 {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("month is required when year is provided"))
	}
	if query.Month != 0 && query.Year == 0 {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("year is required when month is provided"))
	}
	return arTableModel.ListARTableFromDBWithPagination(query)
}

// GetARTable godoc
// @Summary Get AR Table Record by ID
// @Description Get a single AR table record by its primary key ID.
// @Schemes
// @Accept json
// @Produce json
// @Param id path int true "AR table record ID"
// @Success 200 {object} forms.ARTableRecordResponse
// @Failure 400 {object} billing_errors.BaseError
// @Failure 404 {object} billing_errors.BaseError
// @Router /ar_tables/{id} [get]
func (s *ARTableAPI) GetARTable(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	id, err := getARTableIDFromURI(c)
	if err != nil {
		return nil, err
	}
	return arTableModel.GetARTableRecordByID(id)
}

// CreateARTable godoc
// @Summary Create AR Table Record
// @Description Create a new AR table record. All fields except ID (primary key) are supported.
// @Schemes
// @Accept json
// @Produce json
// @Param body body forms.ARTableCreateInput true "AR table record to create"
// @Success 200 {object} forms.ARTableRecordResponse
// @Failure 400 {object} billing_errors.BaseError
// @Router /ar_tables [post]
func (s *ARTableAPI) CreateARTable(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	input := &forms.ARTableCreateInput{}
	if err := c.ShouldBindJSON(input); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	return arTableModel.CreateARTableRecord(input)
}

// UpdateARTable godoc
// @Summary Update AR Table Record
// @Description Update an existing AR table record by ID. Only non-zero fields in the request body will be updated. Primary key (ID) is not allowed to be modified.
// @Schemes
// @Accept json
// @Produce json
// @Param id path int true "AR table record ID"
// @Param body body forms.ARTableUpdateInput true "fields to update (only non-zero fields will be applied)"
// @Success 200 {object} forms.ARTableRecordResponse
// @Failure 400 {object} billing_errors.BaseError
// @Failure 404 {object} billing_errors.BaseError
// @Router /ar_tables/{id} [put]
func (s *ARTableAPI) UpdateARTable(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	id, err := getARTableIDFromURI(c)
	if err != nil {
		return nil, err
	}
	input := &forms.ARTableUpdateInput{}
	if err := c.ShouldBindJSON(input); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	return arTableModel.UpdateARTableRecord(id, input)
}

// DeleteARTable godoc
// @Summary Delete AR Table Record
// @Description Soft-delete an AR table record by its primary key ID.
// @Schemes
// @Accept json
// @Produce json
// @Param id path int true "AR table record ID"
// @Success 200 {object} forms.OKType
// @Failure 400 {object} billing_errors.BaseError
// @Failure 404 {object} billing_errors.BaseError
// @Router /ar_tables/{id} [delete]
func (s *ARTableAPI) DeleteARTable(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	id, err := getARTableIDFromURI(c)
	if err != nil {
		return nil, err
	}
	if err := arTableModel.DeleteARTableRecord(id); err != nil {
		return nil, err
	}
	return forms.OK, nil
}

// DownloadARTables godoc
// @Summary Download AR Table Records as CSV
// @Description Download AR table records as a CSV file with optional filters (year, month, is_estimated, tenant_id, payment_type, binded_billing_config_id).
// @Schemes
// @Accept json
// @Param year query int false "filter by year (used with month, e.g. 2025)"
// @Param month query int false "filter by month (used with year, e.g. 4)"
// @Param is_estimated query bool false "filter by estimated flag"
// @Param tenant_id query int false "filter by tenant ID"
// @Param payment_type query string false "filter by payment type (e.g. stripe, wire_transfer, aws)"
// @Param binded_billing_config_id query int false "filter by binded billing config ID"
// @Success 200 {file} file "CSV file"
// @Failure 400 {object} billing_errors.BaseError
// @Router /ar_tables/download [get]
func (s *ARTableAPI) DownloadARTables(c *gin.Context) {
	query := &forms.ARTableListQuery{}
	if err := c.ShouldBindQuery(query); err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	if query.Year != 0 && query.Month == 0 {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(fmt.Errorf("month is required when year is provided")))
		return
	}
	if query.Month != 0 && query.Year == 0 {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(fmt.Errorf("year is required when month is provided")))
		return
	}
	ret, err := arTableModel.DownloadARTables(query)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	filename := "ar_table.csv"
	if query.Year != 0 && query.Month != 0 {
		filename = fmt.Sprintf("ar_table_%04d-%02d.csv", query.Year, query.Month)
	}
	c.Header("Content-Disposition", "attachment; filename="+filename)
	c.Data(http.StatusOK, "text/csv", ret)
}

func InitializeARTableRouter(router ginutils.MyRoute) {
	s := &ARTableAPI{}
	router.Handle("GET", "/cr_tables/init", s.GetInitARTable)
	router.Handle("GET", "/ar_tables/init", s.GetInitARTable)

	router.HandleWithError("POST", "/ar_tables/trigger", s.InitARTableTrigger)
	router.HandleWithError("POST", "/ar_tables/trigger/estimated", s.InitEstimatedARTableTrigger)
	router.HandleWithError("GET", "/ar_tables/export/status", s.GetARTableExportStatus)
	router.HandleWithError("GET", "/ar_tables/export/download", s.DownloadARTableExport)
	router.Handle("GET", "/ar_tables/download", s.DownloadARTables)
	router.HandleWithError("GET", "/ar_tables", s.ListARTables)
	router.HandleWithError("GET", "/ar_tables/:id", s.GetARTable)
	router.HandleWithError("POST", "/ar_tables", s.CreateARTable)
	router.HandleWithError("PUT", "/ar_tables/:id", s.UpdateARTable)
	router.HandleWithError("DELETE", "/ar_tables/:id", s.DeleteARTable)
}

func InitializeARTableInnerRouter(router ginutils.MyRoute) {
	s := &ARTableAPI{}
	router.HandleWithError("POST", "/ar_tables/trigger", s.InitARTableTrigger)
	router.HandleWithError("POST", "/ar_tables/trigger/estimated", s.InitEstimatedARTableTrigger)
}
