package treasury

import (
	"context"
	"math/big"
	"regexp"
	"strings"
	"time"

	"github.com/tech/sendico/gateway/chsettle/storage"
	storagemodel "github.com/tech/sendico/gateway/chsettle/storage/model"
	"github.com/tech/sendico/pkg/merrors"
)

var treasuryAmountPattern = regexp.MustCompile(`^[0-9]+(\.[0-9]+)?$`)

type LimitKind string

const (
	LimitKindPerOperation LimitKind = "per_operation"
	LimitKindDaily        LimitKind = "daily"
)

type LimitError struct {
	Kind LimitKind
	Max  string
}

func (e *LimitError) Error() string {
	if e == nil {
		return "limit exceeded"
	}
	switch e.Kind {
	case LimitKindPerOperation:
		return "max amount per operation exceeded"
	case LimitKindDaily:
		return "max daily amount exceeded"
	default:
		return "limit exceeded"
	}
}

func (e *LimitError) LimitKind() string {
	if e == nil {
		return ""
	}
	return string(e.Kind)
}

func (e *LimitError) LimitMax() string {
	if e == nil {
		return ""
	}
	return e.Max
}

type Validator struct {
	repo storage.TreasuryRequestsStore

	maxPerOperation *big.Rat
	maxDaily        *big.Rat

	maxPerOperationRaw string
	maxDailyRaw        string
}

func NewValidator(repo storage.TreasuryRequestsStore, maxPerOperation string, maxDaily string) (*Validator, error) {
	validator := &Validator{
		repo:               repo,
		maxPerOperationRaw: strings.TrimSpace(maxPerOperation),
		maxDailyRaw:        strings.TrimSpace(maxDaily),
	}
	if validator.maxPerOperationRaw != "" {
		value, err := parseAmountRat(validator.maxPerOperationRaw)
		if err != nil {
			return nil, merrors.InvalidArgument("treasury max_amount_per_operation is invalid", "treasury.limits.max_amount_per_operation")
		}
		validator.maxPerOperation = value
	}
	if validator.maxDailyRaw != "" {
		value, err := parseAmountRat(validator.maxDailyRaw)
		if err != nil {
			return nil, merrors.InvalidArgument("treasury max_daily_amount is invalid", "treasury.limits.max_daily_amount")
		}
		validator.maxDaily = value
	}
	return validator, nil
}

func (v *Validator) MaxPerOperation() string {
	if v == nil {
		return ""
	}
	return v.maxPerOperationRaw
}

func (v *Validator) MaxDaily() string {
	if v == nil {
		return ""
	}
	return v.maxDailyRaw
}

func (v *Validator) ValidateAmount(amount string) (*big.Rat, string, error) {
	amount = strings.TrimSpace(amount)
	value, err := parseAmountRat(amount)
	if err != nil {
		return nil, "", err
	}
	if v != nil && v.maxPerOperation != nil && value.Cmp(v.maxPerOperation) > 0 {
		return nil, "", &LimitError{
			Kind: LimitKindPerOperation,
			Max:  v.maxPerOperationRaw,
		}
	}
	return value, amount, nil
}

func (v *Validator) ValidateDailyLimit(ctx context.Context, ledgerAccountID string, amount *big.Rat, now time.Time) error {
	if v == nil || v.maxDaily == nil || v.repo == nil {
		return nil
	}
	if amount == nil {
		return merrors.InvalidArgument("amount is required", "amount")
	}
	dayStart := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC)
	dayEnd := dayStart.Add(24 * time.Hour)

	records, err := v.repo.ListByAccountAndStatuses(
		ctx,
		ledgerAccountID,
		[]storagemodel.TreasuryRequestStatus{
			storagemodel.TreasuryRequestStatusCreated,
			storagemodel.TreasuryRequestStatusConfirmed,
			storagemodel.TreasuryRequestStatusScheduled,
			storagemodel.TreasuryRequestStatusExecuted,
		},
		dayStart,
		dayEnd,
	)
	if err != nil {
		return err
	}
	total := new(big.Rat)
	for _, record := range records {
		next, err := parseAmountRat(record.Amount)
		if err != nil {
			return merrors.Internal("treasury request amount is invalid")
		}
		total.Add(total, next)
	}
	total.Add(total, amount)
	if total.Cmp(v.maxDaily) > 0 {
		return &LimitError{
			Kind: LimitKindDaily,
			Max:  v.maxDailyRaw,
		}
	}
	return nil
}

func parseAmountRat(value string) (*big.Rat, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil, merrors.InvalidArgument("amount is required", "amount")
	}
	if !treasuryAmountPattern.MatchString(value) {
		return nil, merrors.InvalidArgument("amount format is invalid", "amount")
	}
	amount := new(big.Rat)
	if _, ok := amount.SetString(value); !ok {
		return nil, merrors.InvalidArgument("amount format is invalid", "amount")
	}
	if amount.Sign() <= 0 {
		return nil, merrors.InvalidArgument("amount must be positive", "amount")
	}
	return amount, nil
}
