package pkg

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

type ExpenseEntry struct {
	Slug          string
	InvoiceDate   time.Time
	InvoiceNumber string
	Amount        string
	InvoicerName  string
	Tags          []string
}

type expensesIndexPage struct {
	Expenses []*ExpenseEntry
	Filters  *expenseFilterForm
}

func IndexExpenses(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	conn := getConn(r)
	locale := getLocale(r)
	company := mustGetCompany(r)
	filters := newExpenseFilterForm(r.Context(), conn, locale, company)
	if err := filters.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	page := &expensesIndexPage{
		Expenses: mustCollectExpenseEntries(r.Context(), conn, company, filters),
		Filters:  filters,
	}
	mustRenderMainTemplate(w, r, "expenses/index.gohtml", page)
}

func mustCollectExpenseEntries(ctx context.Context, conn *Conn, company *Company, filters *expenseFilterForm) []*ExpenseEntry {
	args := []interface{}{company.Id}
	where := []string{"expense.company_id = $1"}
	appendWhere := func(expression string, value interface{}) {
		args = append(args, value)
		where = append(where, fmt.Sprintf(expression, len(args)))
	}
	maybeAppendWhere := func(expression string, value string, conv func(string) interface{}) {
		if value != "" {
			if conv == nil {
				appendWhere(expression, value)
			} else {
				appendWhere(expression, conv(value))
			}
		}
	}
	maybeAppendWhere("contact_id = $%d", filters.Customer.String(), func(v string) interface{} {
		customerId, _ := strconv.Atoi(filters.Customer.Selected[0])
		return customerId
	})
	maybeAppendWhere("invoice_number = $%d", filters.InvoiceNumber.String(), nil)
	maybeAppendWhere("invoice_date >= $%d", filters.FromDate.String(), nil)
	maybeAppendWhere("invoice_date <= $%d", filters.ToDate.String(), nil)
	if len(filters.Tags.Tags) > 0 {
		if filters.TagsCondition.Selected == "and" {
			appendWhere("expense.tags @> $%d", filters.Tags)
		} else {
			appendWhere("expense.tags && $%d", filters.Tags)
		}
	}
	rows := conn.MustQuery(ctx, fmt.Sprintf(`
		select expense.slug
		     , invoice_date
		     , invoice_number
		     , to_price(amount, decimal_digits)
		     , contact.business_name
		     , expense.tags
		from expense
		join contact using (contact_id)
		join currency using (currency_code)
		where (%s)
		order by invoice_date
	`, strings.Join(where, ") AND (")), args...)
	defer rows.Close()

	var entries []*ExpenseEntry
	for rows.Next() {
		entry := &ExpenseEntry{}
		if err := rows.Scan(&entry.Slug, &entry.InvoiceDate, &entry.InvoiceNumber, &entry.Amount, &entry.InvoicerName, &entry.Tags); err != nil {
			panic(err)
		}
		entries = append(entries, entry)
	}
	if rows.Err() != nil {
		panic(rows.Err())
	}

	return entries
}
func ServeExpenseForm(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	locale := getLocale(r)
	conn := getConn(r)
	company := mustGetCompany(r)
	form := newExpenseForm(r.Context(), conn, locale, company)
	slug := params[0].Value
	if slug == "new" {
		w.WriteHeader(http.StatusOK)
		form.InvoiceDate.Val = time.Now().Format("2006-01-02")
		mustRenderNewExpenseForm(w, r, form)
		return
	}
	if !form.MustFillFromDatabase(r.Context(), conn, slug) {
		http.NotFound(w, r)
		return
	}
	w.WriteHeader(http.StatusOK)
	mustRenderEditExpenseForm(w, r, slug, form)
}

func mustRenderNewExpenseForm(w http.ResponseWriter, r *http.Request, form *expenseForm) {
	locale := getLocale(r)
	form.Invoicer.EmptyLabel = gettext("Select a contact.", locale)
	mustRenderMainTemplate(w, r, "expenses/new.gohtml", form)
}

func mustRenderEditExpenseForm(w http.ResponseWriter, r *http.Request, slug string, form *expenseForm) {
	page := &editExpensePage{
		Slug: slug,
		Form: form,
	}
	mustRenderMainTemplate(w, r, "expenses/edit.gohtml", page)
}

type editExpensePage struct {
	Slug string
	Form *expenseForm
}

type expenseForm struct {
	locale        *Locale
	company       *Company
	Invoicer      *SelectField
	InvoiceNumber *InputField
	InvoiceDate   *InputField
	Tax           *SelectField
	Amount        *InputField
	Tags          *TagsField
}

func newExpenseForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseForm {
	return &expenseForm{
		locale:  locale,
		company: company,
		Invoicer: &SelectField{
			Name:     "invoicer",
			Label:    pgettext("input", "Contact", locale),
			Required: true,
			Options:  mustGetContactOptions(ctx, conn, company),
		},
		InvoiceNumber: &InputField{
			Name:  "invoice_number",
			Label: pgettext("input", "Invoice number", locale),
			Type:  "text",
		},
		InvoiceDate: &InputField{
			Name:     "invoice_date",
			Label:    pgettext("input", "Invoice Date", locale),
			Required: true,
			Type:     "date",
		},
		Tax: &SelectField{
			Name:     "tax",
			Label:    pgettext("input", "Taxes", locale),
			Multiple: true,
			Options:  mustGetTaxOptions(ctx, conn, company),
		},
		Amount: &InputField{
			Name:     "amount",
			Label:    pgettext("input", "Amount", locale),
			Type:     "number",
			Required: true,
			Attributes: []template.HTMLAttr{
				`min="0"`,
				template.HTMLAttr(fmt.Sprintf(`step="%v"`, company.MinCents())),
			},
		},
		Tags: &TagsField{
			Name:  "tags",
			Label: pgettext("input", "Tags", locale),
		},
	}
}

func (form *expenseForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	form.Invoicer.FillValue(r)
	form.InvoiceNumber.FillValue(r)
	form.InvoiceDate.FillValue(r)
	form.Tax.FillValue(r)
	form.Amount.FillValue(r)
	form.Tags.FillValue(r)
	return nil
}

func (form *expenseForm) Validate() bool {
	validator := newFormValidator()
	validator.CheckValidSelectOption(form.Invoicer, gettext("Selected contact is not valid.", form.locale))
	validator.CheckValidDate(form.InvoiceDate, gettext("Invoice date must be a valid date.", form.locale))
	validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
	validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
	if validator.CheckRequiredInput(form.Amount, gettext("Amount can not be empty.", form.locale)) {
		validator.CheckValidDecimal(form.Amount, form.company.MinCents(), math.MaxFloat64, gettext("Amount must be a number greater than zero.", form.locale))
	}
	validator.CheckValidSelectOption(form.Tax, gettext("Selected tax is not valid.", form.locale))
	validator.CheckAtMostOneOfEachGroup(form.Tax, gettext("You can only select a tax of each class.", form.locale))
	return validator.AllOK()
}

func (form *expenseForm) MustFillFromDatabase(ctx context.Context, conn *Conn, slug string) bool {
	return !notFoundErrorOrPanic(conn.QueryRow(ctx, `
		select contact_id
		     , invoice_number
		     , invoice_date
		     , to_price(amount, decimal_digits)
		     , array_agg(tax_id)
		     , array_to_string(tags, ',')
		from expense
		left join expense_tax using (expense_id)
		join currency using (currency_code)
		where expense.slug = $1
		group by contact_id
		       , invoice_number
		       , invoice_date
		       , amount
		       , decimal_digits
		       , tags
	`, slug).Scan(
		form.Invoicer,
		form.InvoiceNumber,
		form.InvoiceDate,
		form.Amount,
		form.Tax,
		form.Tags))
}
func HandleAddExpense(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	conn := getConn(r)
	locale := getLocale(r)
	company := mustGetCompany(r)
	form := newExpenseForm(r.Context(), conn, locale, company)
	if err := form.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := verifyCsrfTokenValid(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	if !form.Validate() {
		if !IsHTMxRequest(r) {
			w.WriteHeader(http.StatusUnprocessableEntity)
		}
		mustRenderNewExpenseForm(w, r, form)
		return
	}
	taxes := mustSliceAtoi(form.Tax.Selected)
	conn.MustExec(r.Context(), "select add_expense($1, $2, $3, $4, $5, $6, $7)", company.Id, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags)
	htmxRedirect(w, r, companyURI(company, "/expenses"))
}

func HandleUpdateExpense(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	conn := getConn(r)
	locale := getLocale(r)
	company := mustGetCompany(r)
	form := newExpenseForm(r.Context(), conn, locale, company)
	if err := form.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := verifyCsrfTokenValid(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	slug := params[0].Value
	if !form.Validate() {
		if !IsHTMxRequest(r) {
			w.WriteHeader(http.StatusUnprocessableEntity)
		}
		mustRenderEditExpenseForm(w, r, slug, form)
		return
	}
	taxes := mustSliceAtoi(form.Tax.Selected)
	if found := conn.MustGetText(r.Context(), "", "select edit_expense($1, $2, $3, $4, $5, $6, $7)", slug, form.InvoiceDate, form.Invoicer, form.InvoiceNumber, form.Amount, taxes, form.Tags); found == "" {
		http.NotFound(w, r)
		return
	}
	htmxRedirect(w, r, companyURI(company, "/expenses"))
}

type expenseFilterForm struct {
	locale        *Locale
	company       *Company
	Customer      *SelectField
	InvoiceNumber *InputField
	FromDate      *InputField
	ToDate        *InputField
	Tags          *TagsField
	TagsCondition *ToggleField
}

func newExpenseFilterForm(ctx context.Context, conn *Conn, locale *Locale, company *Company) *expenseFilterForm {
	return &expenseFilterForm{
		locale:  locale,
		company: company,
		Customer: &SelectField{
			Name:       "customer",
			Label:      pgettext("input", "Customer", locale),
			EmptyLabel: gettext("All customers", locale),
			Options:    mustGetContactOptions(ctx, conn, company),
		},
		InvoiceNumber: &InputField{
			Name:  "number",
			Label: pgettext("input", "Invoice Number", locale),
			Type:  "search",
		},
		FromDate: &InputField{
			Name:  "from_date",
			Label: pgettext("input", "From Date", locale),
			Type:  "date",
		},
		ToDate: &InputField{
			Name:  "to_date",
			Label: pgettext("input", "To Date", locale),
			Type:  "date",
		},
		Tags: &TagsField{
			Name:  "tags",
			Label: pgettext("input", "Tags", locale),
		},
		TagsCondition: &ToggleField{
			Name:     "tags_condition",
			Label:    pgettext("input", "Tags Condition", locale),
			Selected: "and",
			FirstOption: &ToggleOption{
				Value:       "and",
				Label:       pgettext("tag condition", "All", locale),
				Description: gettext("Invoices must have all the specified labels.", locale),
			},
			SecondOption: &ToggleOption{
				Value:       "or",
				Label:       pgettext("tag condition", "Any", locale),
				Description: gettext("Invoices must have at least one of the specified labels.", locale),
			},
		},
	}
}

func (form *expenseFilterForm) Parse(r *http.Request) error {
	if err := r.ParseForm(); err != nil {
		return err
	}
	form.Customer.FillValue(r)
	form.InvoiceNumber.FillValue(r)
	form.FromDate.FillValue(r)
	form.ToDate.FillValue(r)
	form.Tags.FillValue(r)
	form.TagsCondition.FillValue(r)
	return nil
}

func ServeEditExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	conn := getConn(r)
	locale := getLocale(r)
	company := getCompany(r)
	slug := params[0].Value
	form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags"), slug, locale)
	if notFoundErrorOrPanic(conn.QueryRow(r.Context(), `select array_to_string(tags, ',') from expense where slug = $1`, form.Slug).Scan(&form.Tags)) {
		http.NotFound(w, r)
		return
	}
	mustRenderStandaloneTemplate(w, r, "tags/edit.gohtml", form)
}

func HandleUpdateExpenseTags(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	locale := getLocale(r)
	conn := getConn(r)
	company := getCompany(r)
	slug := params[0].Value
	form := newTagsForm(companyURI(company, "/expenses/"+slug+"/tags/edit"), slug, locale)
	if err := form.Parse(r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if err := verifyCsrfTokenValid(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	if conn.MustGetText(r.Context(), "", "update expense set tags = $1 where slug = $2 returning slug", form.Tags, form.Slug) == "" {
		http.NotFound(w, r)
	}
	mustRenderStandaloneTemplate(w, r, "tags/view.gohtml", form)
}
