package reconciliation

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"time"

	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"

	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/handler/awshandler"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"go.uber.org/zap"
)

type CheckResult struct {
	ThirdPartyName  string
	Pass            bool
	TenantID        uint
	ThirdPartyCode  string
	PaymentMethodID uint
	Revenue         decimal.Decimal
	Invoice         decimal.Decimal
	InvoiceDetail   map[string]decimal.Decimal
}

const (
	RevenueStructureDetailType_Bills         = "Bills"
	RevenueStructureDetailType_Rest          = "Rest"
	RevenueStructureDetailType_OtherPayment  = "Other Payment"
	RevenueStructureDetailType_RoundingLoss  = "Rounding Loss"
	RevenueStructureDetailType_TechnicalLoss = "Technical Loss"
)

type RevenueStructureDetail struct {
	Type  string
	Date  time.Time
	Value decimal.Decimal
}

type MonthlyCheckResult struct {
	CheckResult
	Bills            decimal.Decimal
	RevenueStructure []*RevenueStructureDetail
}

type Model struct {
}

func (m *Model) Check(
	checkDate time.Time,
	thirdPartyName string,
	invoiceF func(date time.Time, paymentType string) ([]*TenantReconciliationInvoice, error),
	revenueF func(date time.Time) ([]*paymenthandler.TenantRevenue, error),
) ([]*CheckResult, error) {
	var results []*CheckResult
	tenantInvoiceList, err := invoiceF(checkDate, thirdPartyName)
	if err != nil {
		log.Error("failed to get daily invoice on reconciliation", zap.Error(err))
		return nil, err
	}
	tenantRevenueList, err := revenueF(checkDate)
	if err != nil {
		log.Error("failed to get daily revenue on reconciliation", zap.Error(err))
		return nil, err
	}
	tenantRevenueMap := make(map[string]*paymenthandler.TenantRevenue, len(tenantRevenueList))
	paymentModel := &paymentsub.PaymentModel{}
	for _, tr := range tenantRevenueList {
		tenantRevenueMap[tr.PaymentCode] = tr
	}
	processedRevenue := make(map[string]struct{})
	for _, ti := range tenantInvoiceList {
		payment, err := paymentModel.GetPaymentMethod(ti.PaymentMethodID)
		if err != nil {
			return nil, err
		}
		revenue := decimal.Zero
		tr, ok := tenantRevenueMap[payment.PaymentCode]
		if ok {
			revenue = tr.Revenue
		}
		results = append(results, &CheckResult{
			TenantID:        ti.TenantID,
			Pass:            ti.Invoice.Equal(revenue),
			Revenue:         revenue,
			Invoice:         ti.Invoice,
			ThirdPartyCode:  payment.PaymentCode,
			ThirdPartyName:  thirdPartyName,
			InvoiceDetail:   ti.InvoiceDetail,
			PaymentMethodID: ti.PaymentMethodID,
		})
		processedRevenue[payment.PaymentCode] = struct{}{}
	}
	for code, tr := range tenantRevenueMap {
		if _, ok := processedRevenue[code]; !ok {
			results = append(results, &CheckResult{
				TenantID:       0,
				Pass:           false,
				Revenue:        tr.Revenue,
				Invoice:        decimal.Zero,
				ThirdPartyCode: tr.PaymentCode,
				ThirdPartyName: thirdPartyName,
			})
		}
	}
	return results, nil
}

func (m *Model) buildRevenueStructure(result *CheckResult, bills decimal.Decimal, checkMonth time.Time) ([]*RevenueStructureDetail, error) {
	ret := []*RevenueStructureDetail{
		{
			Type:  RevenueStructureDetailType_Bills,
			Date:  checkMonth,
			Value: bills,
		},
	}
	currentMonthInvoice := decimal.Zero
	for month, invoice := range result.InvoiceDetail {
		if month != checkMonth.Format("2006-01") {
			dt, err := time.Parse("2006-01", month)
			if err != nil {
				return nil, err
			}
			ret = append(ret, &RevenueStructureDetail{
				Type:  RevenueStructureDetailType_Rest,
				Date:  dt,
				Value: invoice,
			})
		} else {
			currentMonthInvoice = invoice
		}
	}
	start := timeutils.GetMonthDate(checkMonth)
	end := start.AddDate(0, 1, 0)
	var monthInvoice struct {
		TotalInvoice   decimal.Decimal
		PaymentInvoice decimal.Decimal
	}
	err := db.GetDB().Model(&entity.Invoice{}).
		Where("tenant_id = ?", result.TenantID, result.PaymentMethodID).
		Where("invoice_date >= ? and invoice_date < ?", start, end).
		Select("SUM(cost_price) as total_invoice, SUM(IF(payment_method_id = ?, cost_price, 0)) as payment_invoice").Scan(&monthInvoice).Error
	if err != nil {
		log.Error("failed to retrieve monthly invoice", zap.Error(err))
		return nil, err
	}
	if !monthInvoice.TotalInvoice.Equal(bills) {
		ret = append(ret, &RevenueStructureDetail{
			Type:  RevenueStructureDetailType_RoundingLoss,
			Date:  checkMonth,
			Value: bills.Sub(monthInvoice.TotalInvoice),
		})
	}
	if !monthInvoice.TotalInvoice.Equal(monthInvoice.PaymentInvoice) {
		ret = append(ret, &RevenueStructureDetail{
			Type:  RevenueStructureDetailType_OtherPayment,
			Date:  checkMonth,
			Value: monthInvoice.TotalInvoice.Sub(monthInvoice.PaymentInvoice),
		})
	}
	if !currentMonthInvoice.Equal(monthInvoice.PaymentInvoice) {
		ret = append(ret, &RevenueStructureDetail{
			Type:  RevenueStructureDetailType_Rest,
			Date:  checkMonth,
			Value: currentMonthInvoice.Sub(monthInvoice.PaymentInvoice),
		})
	}
	if !result.Pass {
		ret = append(ret, &RevenueStructureDetail{
			Type:  RevenueStructureDetailType_TechnicalLoss,
			Date:  checkMonth,
			Value: result.Revenue.Sub(result.Invoice),
		})
	}
	return ret, err
}

func (m *Model) BuildMonthlyCheckResult(result *CheckResult, checkMonth time.Time) (*MonthlyCheckResult, error) {
	costExploreModel := costexplorer.CostExplorerModel{}
	// don't need billing config
	cost, err := costExploreModel.GetMonthlyCost(result.TenantID, 0, checkMonth.Year(), int(checkMonth.Month()))
	if err != nil {
		log.Error("failed to get bills", zap.Error(err))
		return nil, err
	}
	revenueStructure, err := m.buildRevenueStructure(result, cost.InvoiceCostsCosts.CostPrice, checkMonth)
	if err != nil {
		log.Error("failed to build revenue structure", zap.Error(err))
		return nil, err
	}
	ret := &MonthlyCheckResult{
		CheckResult:      *result,
		Bills:            cost.InvoiceCostsCosts.CostPrice,
		RevenueStructure: revenueStructure,
	}
	return ret, nil
}

func (m *Model) BuildCSV(header []string, data [][]string) ([]byte, error) {
	buf := new(bytes.Buffer)
	writer := csv.NewWriter(buf)

	err := writer.Write(header)
	if err != nil {
		return nil, err
	}
	for _, row := range data {
		err = writer.Write(row)
		if err != nil {
			return nil, err
		}
	}
	writer.Flush()
	return buf.Bytes(), nil
}

func (m *Model) putReport(key string, body []byte) error {
	s3BucketName := config.Get().GetString(config.ReconciliationReportAWSS3BucketName)
	s3Cli, err := awshandler.NewS3Client()
	if err != nil {
		return err
	}
	resp, err := s3Cli.PutObject(&s3.PutObjectInput{
		Bucket: &s3BucketName,
		Key:    &key,
		Body:   bytes.NewReader(body),
	})
	if err != nil {
		log.Error("failed to put report to s3", zap.String("resp", resp.String()))
		return err
	}
	return nil
}

func (m *Model) reportDailyUnmatchedResult(date time.Time, thirdPartyName string, unmatched []*CheckResult) error {
	header := []string{"Tenant ID", "Marketplace Code", "Total Sent Metering", "Revenue"}
	var data [][]string
	for _, r := range unmatched {
		data = append(data, []string{fmt.Sprint(r.TenantID), r.ThirdPartyCode, r.Invoice.String(), r.Revenue.String()})
	}
	raw, err := m.BuildCSV(header, data)
	if err != nil {
		log.Error("failed to build csv", zap.Error(err))
		return err
	}
	key := fmt.Sprintf("%s/daily/%s/daily_%s_%s_%s.csv", config.Get().GetString(config.ENV), date.Format("2006-01-02"), thirdPartyName, config.Get().GetString(config.ENV), date.Format("2006-01-02"))
	return m.putReport(key, raw)
}

func (m *Model) BuildMonthlyReport(date time.Time, thirdPartyName string, report []*MonthlyCheckResult) ([]byte, error) {
	header := []string{"Tenant ID", "Tenant Name", "Marketplace Code", "Bills", "Total Sent Metering", "Revenue", "Check Result", "Revenue Structure"}
	var data [][]string
	for _, r := range report {
		var tenantName string
		tenant, err := centralhandler.GetCentralHandler().GetTenant(r.TenantID)
		if err == nil && tenant != nil {
			tenantName = tenant.Name
		} else {
			log.Error("failed to get tenants in worker.", zap.Error(err))
		}
		data = append(data, []string{fmt.Sprint(r.TenantID), tenantName, r.ThirdPartyCode, r.Bills.String(), r.Revenue.String(), r.Invoice.String(), fmt.Sprint(r.Pass),
			fmt.Sprintf("$%s (Revenue in %s) = ", r.Revenue.String(), date.Format("2006-01"))})
		for _, detail := range r.RevenueStructure {
			empty_fill := []string{"", "", "", "", "", "", "", ""}
			symbol := "+"
			if detail.Value.LessThan(decimal.Zero) {
				symbol = "-"
			}
			var row []string
			if detail.Type == RevenueStructureDetailType_Bills || detail.Type == RevenueStructureDetailType_Rest {
				row = append(empty_fill, fmt.Sprintf("%s$%s (%s in %s)", symbol, detail.Value.Abs(), detail.Type, detail.Date.Format("2006-01")))
			} else {
				row = append(empty_fill, fmt.Sprintf("%s$%s (%s)", symbol, detail.Value.Abs(), detail.Type))
			}
			data = append(data, row)
		}
	}
	raw, err := m.BuildCSV(header, data)
	if err != nil {
		log.Error("failed to build csv", zap.Error(err), zap.String("date", date.Format("2006-01")), zap.String("thirdPartyName", thirdPartyName))
		return nil, err
	}
	return raw, nil
}

func (m *Model) putMonthlyReport(date time.Time, thirdPartyName string, report []*MonthlyCheckResult) error {
	raw, err := m.BuildMonthlyReport(date, thirdPartyName, report)
	if err != nil {
		return err
	}
	key := fmt.Sprintf("%s/montly/%s/montly_%s_%s_%s.csv", config.Get().GetString(config.ENV), date.Format("2006-01"), thirdPartyName, config.Get().GetString(config.ENV), date.Format("2006-01"))
	return m.putReport(key, raw)
}

func (m *Model) RunReconciliationAll(process func(provider paymenthandler.PaymentProvider) error) error {
	var lastErr error
	for _, providerName := range paymenthandler.GetPaymentHandler().AllProviders() {
		h, err := paymenthandler.GetPaymentHandler().GetProvider(providerName)
		if err != nil {
			log.Error("err occurred on daily reconciliation", zap.Error(err))
			lastErr = err
			continue
		}
		if !h.EnableReconciliation() {
			log.Info("reconciliation not enabled, skip it", zap.String("providerName", providerName))
			continue
		}
		err = process(h)
		if err != nil {
			log.Error("failed run process func", zap.Error(err), zap.String("providerName", providerName))
			lastErr = err
			continue
		}
	}
	return lastErr
}

func (m *Model) DailyCheck() error {
	now := timeutils.GetNow()
	delay := config.Get().GetInt(config.DailyReconciliationDelayDays)
	checkDate := timeutils.GetDayDate(now.AddDate(0, 0, -delay))
	err := m.RunReconciliationAll(func(provider paymenthandler.PaymentProvider) error {
		results, err := m.Check(checkDate, provider.GetThirdPartyName(), GetDailyTotalInvoice, provider.GetDailyRevenue)
		if err != nil {
			log.Error("err occurred on daily reconciliation", zap.Error(err))
			return err
		}
		var unmatchedRevenue []*CheckResult
		for _, ret := range results {
			if !ret.Pass {
				unmatchedRevenue = append(unmatchedRevenue, ret)
			}
		}
		if len(unmatchedRevenue) != 0 {
			log.Error("unmatched revenue detected, please check in time!", zap.String("provider", provider.GetThirdPartyName()), zap.Int("unmatchedRevenueRows", len(unmatchedRevenue)), zap.Time("checkDate", checkDate))
			err = m.reportDailyUnmatchedResult(checkDate, provider.GetThirdPartyName(), unmatchedRevenue)
			if err != nil {
				log.Error("failed to send daily report email", zap.Error(err))
				return err
			}
		}
		return nil
	})
	return err
}

func (m *Model) MonthlyReport() error {
	now := timeutils.GetNow()
	delay := config.Get().GetInt(config.MonthlyReconciliationDelayDays)
	lastMonth := timeutils.GetLastMonth(now)
	checkMonth := timeutils.GetLastMonth(now.AddDate(0, 0, -delay))
	if !lastMonth.Equal(checkMonth) {
		log.Info("Too early to start last month reconciliation")
		return nil
	}
	err := m.RunReconciliationAll(func(provider paymenthandler.PaymentProvider) error {
		results, err := m.Check(checkMonth, provider.GetThirdPartyName(), GetMonthlyTotalInvoice, provider.GetMonthlyRevenue)
		if err != nil {
			log.Error("err occurred on monthly reconciliation", zap.Error(err), zap.Time("checkMonth", checkMonth))
			return err
		}
		report := make([]*MonthlyCheckResult, len(results))
		for index, res := range results {
			monthlyResult, err := m.BuildMonthlyCheckResult(res, checkMonth)
			if err != nil {
				log.Error("failed to build MonthlyCheckResult", zap.Error(err), zap.Time("checkMonth", checkMonth))
				return err
			}
			report[index] = monthlyResult
		}
		err = m.putMonthlyReport(checkMonth, provider.GetThirdPartyName(), report)
		if err != nil {
			log.Error("failed to put monthly reconciliation report", zap.Error(err))
			return err
		}
		return nil
	})
	return err
}
