package pkg

import (
	"context"
	"fmt"
	"github.com/julienschmidt/httprouter"
	"html/template"
	"math"
	"net/http"
	"strings"
	"time"
)

const (
	MonthPeriod         = "month"
	YestermonthPeriod   = "yestermonth"
	QuarterPeriod       = "quarter"
	YesterquarterPeriod = "yesterquarter"
	YearPeriod          = "year"
	YesteryearPeriod    = "yesteryear"
)

type DashboardPage struct {
	Sales     string
	Income    string
	Expenses  string
	VAT       string
	IRPF      string
	NetIncome string
	Chart     template.HTML
	Filters   *dashboardFilterForm
}

func ServeDashboard(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	company := mustGetCompany(r)
	locale := getLocale(r)
	filters := newDashboardFilterForm(locale, company)
	if err := filters.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	periodStart := "date_trunc('month', current_date)::date"
	periodEnd := "current_date"
	switch filters.Period.Selected {
	case YestermonthPeriod:
		periodStart = "date_trunc('month', current_date - interval '1 month')::date"
		periodEnd = "(date_trunc('month', current_date) - interval '1 day')::date"
	case QuarterPeriod:
		periodStart = "date_trunc('quarter', current_date)::date"
	case YesterquarterPeriod:
		periodStart = "date_trunc('quarter', current_date - interval '3 months')::date"
		periodEnd = "(date_trunc('quarter', current_date) - interval '1 day')::date"
	case YearPeriod:
		periodStart = "date_trunc('year', current_date)::date"
	case YesteryearPeriod:
		periodStart = "date_trunc('year', current_date - interval '1 year')::date"
		periodEnd = "(date_trunc('year', current_date) - interval '1 day')::date"
	case "":
		filters.Period.Selected = MonthPeriod
	}
	conn := getConn(r)
	dashboard := &DashboardPage{
		Filters: filters,
		Chart:   buildDashboardChart(r.Context(), conn, locale, company, periodStart, periodEnd, filters.Period.Selected),
	}
	rows := conn.MustQuery(r.Context(), fmt.Sprintf(`
		select to_price(0, decimal_digits) as sales
		     , to_price(coalesce(invoice.total, 0), decimal_digits) as income
		     , to_price(coalesce(expense.total, 0), decimal_digits) as expenses
		     , to_price(coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0), decimal_digits) as vat
		     , to_price(coalesce(invoice_tax.irpf, 0) + coalesce(expense_tax.irpf, 0), decimal_digits) as irpf
		     , to_price(coalesce(invoice.total, 0) - coalesce(expense.total, 0) - (coalesce(invoice_tax.vat, 0) - coalesce(expense_tax.vat, 0)) + coalesce(expense_tax.irpf, 0), decimal_digits) as net_income
		from company
		    left join (
		        select company_id, sum(total)::integer as total
		        from invoice
		    	join invoice_amount using (invoice_id)
		        where invoice_date between %[1]s and %[2]s
		        group by company_id
		    ) as invoice using (company_id)
		    left join (
		        select company_id, sum(amount)::integer as total
		        from expense
		        where invoice_date between %[1]s and %[2]s
		        group by company_id
		    ) as expense using (company_id)
		    left join (
		        select invoice.company_id
		             , sum(case when tax_class.name = 'IVA' then invoice_tax_amount.amount else 0 end)::integer as vat
		             , sum(case when tax_class.name = 'IRPF' then invoice_tax_amount.amount else 0 end)::integer as irpf
		        from invoice
		        join invoice_tax_amount using (invoice_id)
		        join tax using (tax_id)
		        join tax_class using (tax_class_id)
		        where invoice_date between %[1]s and %[2]s
		        group by invoice.company_id
		    ) as invoice_tax using (company_id)
		    left join (
		        select expense.company_id
		             , sum(case when tax_class.name = 'IVA' then expense_tax_amount.amount else 0 end)::integer as vat
		             , sum(case when tax_class.name = 'IRPF' then expense_tax_amount.amount else 0 end)::integer as irpf
		        from expense
		        join expense_tax_amount using (expense_id)
		        join tax using (tax_id)
		        join tax_class using (tax_class_id)
		        where invoice_date between %[1]s and %[2]s
		        group by expense.company_id
		    ) as expense_tax using (company_id)
		join currency using (currency_code)
		where company_id = $1
	`, periodStart, periodEnd), company.Id)
	defer rows.Close()

	for rows.Next() {
		if err := rows.Scan(&dashboard.Sales, &dashboard.Income, &dashboard.Expenses, &dashboard.VAT, &dashboard.IRPF, &dashboard.NetIncome); err != nil {
			panic(err)
		}
	}
	if rows.Err() != nil {
		panic(rows.Err())
	}
	mustRenderMainTemplate(w, r, "dashboard.gohtml", dashboard)
}

type dashboardFilterForm struct {
	locale  *Locale
	company *Company
	Period  *RadioField
}

func newDashboardFilterForm(locale *Locale, company *Company) *dashboardFilterForm {
	return &dashboardFilterForm{
		locale:  locale,
		company: company,
		Period: &RadioField{
			Name:     "period",
			Selected: MonthPeriod,
			Label:    pgettext("input", "Period", locale),
			Options: []*RadioOption{
				{
					Label: pgettext("period option", "Month", locale),
					Value: MonthPeriod,
				},
				{
					Label: pgettext("period option", "Previous month", locale),
					Value: YestermonthPeriod,
				},
				{
					Label: pgettext("period option", "Quarter", locale),
					Value: QuarterPeriod,
				},
				{
					Label: pgettext("period option", "Previous quarter", locale),
					Value: YesterquarterPeriod,
				},
				{
					Label: pgettext("period option", "Year", locale),
					Value: YearPeriod,
				},
				{
					Label: pgettext("period option", "Previous year", locale),
					Value: YesteryearPeriod,
				},
			},
		},
	}
}

func (form *dashboardFilterForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	form.Period.FillValue(r)
	return nil
}

func buildDashboardChart(ctx context.Context, conn *Conn, locale *Locale, company *Company, periodStart string, periodEnd string, selectedPeriod string) template.HTML {
	group := "yyyymmdd"
	dateFormat := "02/01/2006"
	switch selectedPeriod {
	case YearPeriod, YesteryearPeriod:
		group = "yyyymm"
		dateFormat = "01/2006"
	}
	rows := conn.MustQuery(ctx, fmt.Sprintf(`
      select date
           , to_date(date::text, '%[3]s')
           , 0 as sales
           , to_price(0, $2) as sales_price
           , coalesce(invoice.total, 0) as income
           , to_price(coalesce(invoice.total, 0), $2) as income_price
           , coalesce(expense.total, 0) as expenses
           , to_price(coalesce(expense.total, 0), $2) as expenses_price
      from (
      	select to_char(date.invoice_date, '%[3]s')::integer as date
      	     , sum(total)::integer as total
            from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
        	  	left join invoice on invoice.invoice_date = date.invoice_date and company_id = $1
                left join invoice_amount using (invoice_id)
            group by date
        ) as invoice
        left join (
        	select to_char(date.invoice_date, '%[3]s')::integer as date
        	     , sum(amount)::integer as total
            from generate_series(%[1]s, %[2]s, interval '1 day') as date(invoice_date)
            left join expense on expense.invoice_date = date.invoice_date and company_id = $1
            group by date
        ) as expense using (date)
      order by date
	`, periodStart, periodEnd, group), company.Id, company.DecimalDigits)
	defer rows.Close()

	type value struct {
		index         int
		date          time.Time
		sales         float64
		salesPrice    string
		income        float64
		incomePrice   string
		expenses      float64
		expensesPrice string
	}
	var values []value
	var max = 0.
	for rows.Next() {
		var v value
		var sales int
		var income int
		var expenses int
		if err := rows.Scan(&v.index, &v.date, &sales, &v.salesPrice, &income, &v.incomePrice, &expenses, &v.expensesPrice); err != nil {
			panic(err)
		}
		v.sales = float64(sales)
		v.income = float64(income)
		v.expenses = float64(expenses)
		values = append(values, v)
		max = math.Max(v.sales, max)
		max = math.Max(v.income, max)
		max = math.Max(v.expenses, max)
	}
	if max == 0 {
		max = 1
	}
	if rows.Err() != nil {
		panic(rows.Err())
	}

	width := 1024.
	height := 256.
	dataPoints := float64(len(values)) - 1
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("<svg id='income-chart' viewBox='-10 -10 %d %d'>", int(width)+20, int(height)+20))
	sb.WriteString("<rect x='-10' y='-10' width='100%', height='100%'/>")
	writePolyline := func(value func(value) float64) {
		sb.WriteString("<polyline points='")
		for i, v := range values {
			sb.WriteString(fmt.Sprintf(" %f,%f", float64(i)/dataPoints*width, height-value(v)/max*height))
		}
		sb.WriteString("'/>")
	}
	writePolyline(func(v value) float64 { return v.sales })
	writePolyline(func(v value) float64 { return v.income })
	writePolyline(func(v value) float64 { return v.expenses })

	writeCircle := func(i int, time time.Time, p string, v float64) {
		price := formatPrice(p, locale.Language, locale.CurrencyPattern, company.DecimalDigits, company.CurrencySymbol)
		date := time.Format(dateFormat)
		sb.WriteString(fmt.Sprintf("<circle cx='%f' cy='%f' r='4'><title>%s\n%s</title></circle>", float64(i)/dataPoints*width, height-v/max*height, date, price))
	}
	for i, v := range values {
		writeCircle(i, v.date, v.salesPrice, v.sales)
		writeCircle(i, v.date, v.incomePrice, v.income)
		writeCircle(i, v.date, v.expensesPrice, v.expenses)
	}
	sb.WriteString("</svg>")
	return template.HTML(sb.String())
}
