package server

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

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"scrabble/backend/internal/adminconsole"
	"scrabble/backend/internal/mynalogsync"
	"scrabble/backend/internal/payments"
)

// myNalogBack is where every action on the tax-export page returns to.
const myNalogBack = "/_gm/mynalog"

// myNalogListLimit caps each of the three working lists on the page.
const myNalogListLimit = 200

// myNalogOverdueDay is the day of the following month by which a closed month's income must have
// been filed at the very latest. It is shown as a warning, not enforced.
const myNalogOverdueDay = 9

// consoleMyNalog renders the tax-export section: the session, what is owed, and the manual fallback.
func (s *Server) consoleMyNalog(c *gin.Context) {
	ctx := c.Request.Context()
	loc := s.mynalog.Location()
	view := adminconsole.MyNalogView{TimeZone: loc.String()}

	stored, ok, err := s.payments.MyNalogSession(ctx)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	if ok {
		view.SignedIn = true
		view.INN = stored.INN
		view.AutoEnabled = stored.AutoEnabled
		view.Paused = stored.Paused()
		view.PauseReason = stored.PauseReason
		if stored.LastOKAt != nil {
			view.LastOK = stored.LastOKAt.In(loc).Format("2006-01-02 15:04")
		}
	}

	queue, err := s.payments.MyNalogQueue(ctx, myNalogListLimit)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	for _, in := range queue.Incomes {
		view.Owed = append(view.Owed, s.myNalogRow(in, stored.INN, loc))
	}
	for _, in := range queue.Attention {
		view.Attention = append(view.Attention, s.myNalogRow(in, stored.INN, loc))
	}
	for _, cn := range queue.Cancels {
		view.Cancels = append(view.Cancels, adminconsole.MyNalogCancelRow{
			LedgerID:     cn.LedgerID.String(),
			AccountID:    cn.AccountID.String(),
			AccountShort: shortID(cn.AccountID.String()),
			ReceiptUUID:  cn.ReceiptUUID,
			Failed:       cn.CancelStatus == payments.MyNalogCancelFailed,
		})
	}

	now := time.Now().In(loc)
	monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, loc)
	backlog, err := s.payments.MyNalogBacklogBefore(ctx, monthStart)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	if backlog.Count > 0 {
		view.Backlog = adminconsole.MyNalogBacklogRow{
			Count:   backlog.Count,
			Amount:  fmtMinor(backlog.AmountMinor, payments.CurrencyRUB),
			Oldest:  backlog.Oldest.In(loc).Format("2006-01-02"),
			Overdue: now.Day() >= myNalogOverdueDay,
		}
	}
	s.renderConsole(c, "mynalog", "mynalog", "My Nalog", view)
}

// fmtMinor renders a minor-unit total for display, or "" when the currency is not one we know.
func fmtMinor(minor int64, cur payments.Currency) string {
	m, err := payments.MoneyFromMinor(minor, cur)
	if err != nil {
		return ""
	}
	return fmtMoney(m)
}

// myNalogRow projects one income into its console row, including the printable receipt link once
// the tax service has issued one.
func (s *Server) myNalogRow(in payments.MyNalogIncome, inn string, loc *time.Location) adminconsole.MyNalogRow {
	row := adminconsole.MyNalogRow{
		LedgerID:     in.LedgerID.String(),
		AccountID:    in.AccountID.String(),
		AccountShort: shortID(in.AccountID.String()),
		At:           in.OperationTime.In(loc).Format("2006-01-02 15:04"),
		Amount:       in.Amount.String(),
		Name:         s.mynalog.ReceiptName(in),
		Status:       in.Status,
		Attempts:     in.Attempts,
		LastError:    in.LastError,
		ReceiptUUID:  in.ReceiptUUID,
	}
	if in.ReceiptUUID != "" && inn != "" {
		row.ReceiptURL = s.mynalog.ReceiptURL(inn, in.ReceiptUUID)
	}
	return row
}

// consoleMyNalogSignIn exchanges the tax-cabinet password for a session. The password is read from
// the form, used once and never stored: only the refresh token it yields is kept, encrypted.
func (s *Server) consoleMyNalogSignIn(c *gin.Context) {
	login, password := trimForm(c, "login"), c.PostForm("password")
	if login == "" || password == "" {
		s.renderConsoleMessage(c, "Missing credentials",
			"both the tax cabinet login (your INN) and the password are required", myNalogBack)
		return
	}
	if err := s.mynalog.SignIn(c.Request.Context(), login, password); err != nil {
		// The error text comes from the tax service and never carries the credentials back.
		s.renderConsoleMessage(c, "Sign-in failed", err.Error(), myNalogBack)
		return
	}
	s.renderConsoleMessage(c, "Signed in",
		"the tax cabinet session is stored; only its refresh token was kept, not the password",
		myNalogBack)
}

// consoleMyNalogSignOut forgets the stored session, which also disarms the automatic export.
func (s *Server) consoleMyNalogSignOut(c *gin.Context) {
	if err := s.mynalog.SignOut(c.Request.Context()); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Signed out",
		"the stored session is gone and the automatic export is off", myNalogBack)
}

// consoleMyNalogRun performs one export run and reports what it did.
func (s *Server) consoleMyNalogRun(c *gin.Context) {
	sum, err := s.mynalog.RunBatch(c.Request.Context(), mynalogsync.ConsoleBudget)
	if err != nil {
		if errors.Is(err, mynalogsync.ErrNotSignedIn) {
			s.renderConsoleMessage(c, "Not signed in",
				"sign in to the tax cabinet before running an export", myNalogBack)
			return
		}
		s.consoleError(c, err)
		return
	}
	switch {
	case sum.Busy:
		s.renderConsoleMessage(c, "Already running",
			"another export is in progress; wait for it to finish", myNalogBack)
		return
	case sum.Paused:
		s.renderConsoleMessage(c, "Export is paused", sum.Stopped, myNalogBack)
		return
	}
	body := fmt.Sprintf("filed %d, annulled %d, ruled out %d, failed %d, unresolved %d",
		sum.Registered, sum.Cancelled, sum.NotRequired, sum.Failed, sum.Unknown)
	if sum.Stopped != "" {
		body += ". " + sum.Stopped
	}
	s.renderConsoleMessage(c, "Export finished", body, myNalogBack)
}

// consoleMyNalogAuto arms or disarms the automatic export.
func (s *Server) consoleMyNalogAuto(c *gin.Context) {
	enabled := c.PostForm("enabled") == "1"
	if err := s.payments.SetMyNalogAuto(c.Request.Context(), enabled); err != nil {
		s.consoleError(c, err)
		return
	}
	body := "the automatic export is off; nothing is filed until you press Run"
	if enabled {
		body = fmt.Sprintf("the automatic export is on; it wakes every %s and does nothing when "+
			"there is nothing to file", mynalogsync.WorkerInterval)
	}
	s.renderConsoleMessage(c, "Saved", body, myNalogBack)
}

// consoleMyNalogResume clears a self-imposed pause once the operator has dealt with its cause.
func (s *Server) consoleMyNalogResume(c *gin.Context) {
	if err := s.payments.ResumeMyNalog(c.Request.Context()); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Resumed", "the export will run again on the next tick", myNalogBack)
}

// consoleMyNalogRecheck asks the tax service once more whether a receipt with an unresolved outcome
// exists after all.
func (s *Server) consoleMyNalogRecheck(c *gin.Context) {
	id, ok := s.myNalogLedgerID(c)
	if !ok {
		return
	}
	found, err := s.mynalog.Recheck(c.Request.Context(), id)
	if err != nil {
		s.renderConsoleMessage(c, "Re-check failed", err.Error(), myNalogBack)
		return
	}
	if found {
		s.renderConsoleMessage(c, "Found",
			"the receipt does exist; the income is now recorded as filed", myNalogBack)
		return
	}
	s.renderConsoleMessage(c, "Not found",
		"the tax service still does not list this receipt. If you are sure it was never created, "+
			"return the income to the queue; otherwise file it by hand and enter its number here",
		myNalogBack)
}

// consoleMyNalogRequeue puts an unresolved income back into the queue. It is the operator asserting
// what the software may not assume on its own: that the receipt really was never created.
func (s *Server) consoleMyNalogRequeue(c *gin.Context) {
	id, ok := s.myNalogLedgerID(c)
	if !ok {
		return
	}
	if err := s.mynalog.Requeue(c.Request.Context(), id); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Queued", "the income will be filed on the next run", myNalogBack)
}

// consoleMyNalogManual records an income the operator filed by hand, together with the receipt
// number the tax cabinet gave it. That number is what lets a later refund annul the receipt
// automatically, which is why it is asked for rather than a simple "done" tick.
func (s *Server) consoleMyNalogManual(c *gin.Context) {
	id, ok := s.myNalogLedgerID(c)
	if !ok {
		return
	}
	receipt := trimForm(c, "receipt_uuid")
	if receipt == "" {
		s.renderConsoleMessage(c, "Missing receipt number",
			"enter the receipt number the tax cabinet shows for this income", myNalogBack)
		return
	}
	if err := s.mynalog.RecordManual(c.Request.Context(), id, receipt); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Recorded",
		"the income counts as filed; a refund will annul this receipt automatically", myNalogBack)
}

// consoleMyNalogSkip rules an income out of the export for a reason the operator gives.
func (s *Server) consoleMyNalogSkip(c *gin.Context) {
	id, ok := s.myNalogLedgerID(c)
	if !ok {
		return
	}
	reason := trimForm(c, "reason")
	if reason == "" {
		reason = "ruled out by an operator"
	}
	if err := s.mynalog.Skip(c.Request.Context(), id, reason); err != nil {
		s.consoleError(c, err)
		return
	}
	s.renderConsoleMessage(c, "Ruled out", "this income will not be filed", myNalogBack)
}

// myNalogLedgerID parses the posted ledger id.
func (s *Server) myNalogLedgerID(c *gin.Context) (uuid.UUID, bool) {
	id, err := uuid.Parse(trimForm(c, "ledger_id"))
	if err != nil {
		s.renderConsoleMessage(c, "Invalid id", "the ledger id is not valid", myNalogBack)
		return uuid.UUID{}, false
	}
	return id, true
}

// consoleMyNalogExport writes the outstanding income as CSV in the shape the tax cabinet's own form
// expects — the date, the service description and the amount.
//
// This is the fallback for the API being unavailable or having changed, which for an undocumented
// interface is a matter of when rather than if. The ledger id travels with each row so that the
// receipt number can be entered back on the page afterwards, which is what keeps a hand-filed
// income annullable on a refund.
func (s *Server) consoleMyNalogExport(c *gin.Context) {
	queue, err := s.payments.MyNalogQueue(c.Request.Context(), myNalogListLimit)
	if err != nil {
		s.consoleError(c, err)
		return
	}
	loc := s.mynalog.Location()
	c.Header("Content-Type", "text/csv; charset=utf-8")
	c.Header("Content-Disposition", `attachment; filename="mynalog.csv"`)
	w := csv.NewWriter(c.Writer)
	_ = w.Write([]string{"operation_time", "service_name", "amount", "currency", "status", "ledger_id"})
	for _, in := range append(queue.Incomes, queue.Attention...) {
		_ = w.Write([]string{
			in.OperationTime.In(loc).Format("2006-01-02 15:04:05"),
			csvSafe(s.mynalog.ReceiptName(in)),
			in.Amount.Major(),
			string(in.Amount.Currency()),
			in.Status,
			in.LedgerID.String(),
		})
	}
	w.Flush()
}

// myNalogEnabled reports whether the tax-export console section should exist.
func (s *Server) myNalogEnabled() bool { return s.mynalog != nil && s.payments != nil }

// registerMyNalogConsole attaches the tax-export section to the console group.
func (s *Server) registerMyNalogConsole(gm *gin.RouterGroup) {
	if !s.myNalogEnabled() {
		return
	}
	gm.GET("/mynalog", s.consoleMyNalog)
	gm.GET("/mynalog.csv", s.consoleMyNalogExport)
	gm.POST("/mynalog/login", s.consoleMyNalogSignIn)
	gm.POST("/mynalog/logout", s.consoleMyNalogSignOut)
	gm.POST("/mynalog/run", s.consoleMyNalogRun)
	gm.POST("/mynalog/auto", s.consoleMyNalogAuto)
	gm.POST("/mynalog/resume", s.consoleMyNalogResume)
	gm.POST("/mynalog/recheck", s.consoleMyNalogRecheck)
	gm.POST("/mynalog/requeue", s.consoleMyNalogRequeue)
	gm.POST("/mynalog/manual", s.consoleMyNalogManual)
	gm.POST("/mynalog/skip", s.consoleMyNalogSkip)
}
