package repo

import (
	"context"
	"errors"
	"time"

	"github.com/google/uuid"
	"github.com/hay-kot/homebox/backend/internal/data/ent"
	"github.com/hay-kot/homebox/backend/internal/data/ent/group"
	"github.com/hay-kot/homebox/backend/internal/data/ent/item"
	"github.com/hay-kot/homebox/backend/internal/data/ent/maintenanceentry"
	"github.com/hay-kot/homebox/backend/internal/data/types"
)

// MaintenanceEntryRepository is a repository for maintenance entries that are
// associated with an item in the database. An entry represents a maintenance event
// that has been performed on an item.
type MaintenanceEntryRepository struct {
	db *ent.Client
}

type MaintenanceEntryCreate struct {
	CompletedDate types.Date `json:"completedDate"`
	ScheduledDate types.Date `json:"scheduledDate"`
	Name          string     `json:"name"          validate:"required"`
	Description   string     `json:"description"`
	Cost          float64    `json:"cost,string"`
}

func (mc MaintenanceEntryCreate) Validate() error {
	if mc.CompletedDate.Time().IsZero() && mc.ScheduledDate.Time().IsZero() {
		return errors.New("either completedDate or scheduledDate must be set")
	}
	return nil
}

type MaintenanceEntryUpdate struct {
	CompletedDate types.Date `json:"completedDate"`
	ScheduledDate types.Date `json:"scheduledDate"`
	Name          string     `json:"name"`
	Description   string     `json:"description"`
	Cost          float64    `json:"cost,string"`
}

func (mu MaintenanceEntryUpdate) Validate() error {
	if mu.CompletedDate.Time().IsZero() && mu.ScheduledDate.Time().IsZero() {
		return errors.New("either completedDate or scheduledDate must be set")
	}
	return nil
}

type (
	MaintenanceEntry struct {
		ID            uuid.UUID  `json:"id"`
		CompletedDate types.Date `json:"completedDate"`
		ScheduledDate types.Date `json:"scheduledDate"`
		Name          string     `json:"name"`
		Description   string     `json:"description"`
		Cost          float64    `json:"cost,string"`
	}

	MaintenanceLog struct {
		ItemID      uuid.UUID          `json:"itemId"`
		CostAverage float64            `json:"costAverage"`
		CostTotal   float64            `json:"costTotal"`
		Entries     []MaintenanceEntry `json:"entries"`
	}
)

var (
	mapMaintenanceEntryErr  = mapTErrFunc(mapMaintenanceEntry)
	mapEachMaintenanceEntry = mapTEachFunc(mapMaintenanceEntry)
)

func mapMaintenanceEntry(entry *ent.MaintenanceEntry) MaintenanceEntry {
	return MaintenanceEntry{
		ID:            entry.ID,
		CompletedDate: types.Date(entry.Date),
		ScheduledDate: types.Date(entry.ScheduledDate),
		Name:          entry.Name,
		Description:   entry.Description,
		Cost:          entry.Cost,
	}
}

func (r *MaintenanceEntryRepository) GetScheduled(ctx context.Context, GID uuid.UUID, dt types.Date) ([]MaintenanceEntry, error) {
	entries, err := r.db.MaintenanceEntry.Query().
		Where(
			maintenanceentry.HasItemWith(
				item.HasGroupWith(group.ID(GID)),
			),
			maintenanceentry.ScheduledDate(dt.Time()),
			maintenanceentry.Or(
				maintenanceentry.DateIsNil(),
				maintenanceentry.DateEQ(time.Time{}),
			),
		).
		All(ctx)

	if err != nil {
		return nil, err
	}

	return mapEachMaintenanceEntry(entries), nil
}

func (r *MaintenanceEntryRepository) Create(ctx context.Context, itemID uuid.UUID, input MaintenanceEntryCreate) (MaintenanceEntry, error) {
	item, err := r.db.MaintenanceEntry.Create().
		SetItemID(itemID).
		SetDate(input.CompletedDate.Time()).
		SetScheduledDate(input.ScheduledDate.Time()).
		SetName(input.Name).
		SetDescription(input.Description).
		SetCost(input.Cost).
		Save(ctx)

	return mapMaintenanceEntryErr(item, err)
}

func (r *MaintenanceEntryRepository) Update(ctx context.Context, ID uuid.UUID, input MaintenanceEntryUpdate) (MaintenanceEntry, error) {
	item, err := r.db.MaintenanceEntry.UpdateOneID(ID).
		SetDate(input.CompletedDate.Time()).
		SetScheduledDate(input.ScheduledDate.Time()).
		SetName(input.Name).
		SetDescription(input.Description).
		SetCost(input.Cost).
		Save(ctx)

	return mapMaintenanceEntryErr(item, err)
}

type MaintenanceLogQuery struct {
	Completed bool `json:"completed" schema:"completed"`
	Scheduled bool `json:"scheduled" schema:"scheduled"`
}

func (r *MaintenanceEntryRepository) GetLog(ctx context.Context, groupID, itemID uuid.UUID, query MaintenanceLogQuery) (MaintenanceLog, error) {
	log := MaintenanceLog{
		ItemID: itemID,
	}

	q := r.db.MaintenanceEntry.Query().Where(
		maintenanceentry.ItemID(itemID),
		maintenanceentry.HasItemWith(
			item.HasGroupWith(group.IDEQ(groupID)),
		),
	)

	if query.Completed {
		q = q.Where(maintenanceentry.And(
			maintenanceentry.DateNotNil(),
			maintenanceentry.DateNEQ(time.Time{}),
		))
	} else if query.Scheduled {
		q = q.Where(maintenanceentry.And(
			maintenanceentry.Or(
				maintenanceentry.DateIsNil(),
				maintenanceentry.DateEQ(time.Time{}),
			),
			maintenanceentry.ScheduledDateNotNil(),
			maintenanceentry.ScheduledDateNEQ(time.Time{}),
		))
	}

	entries, err := q.Order(ent.Desc(maintenanceentry.FieldDate)).
		All(ctx)
	if err != nil {
		return MaintenanceLog{}, err
	}

	log.Entries = mapEachMaintenanceEntry(entries)

	var maybeTotal *float64
	var maybeAverage *float64

	statement := `
SELECT
  SUM(cost_total) AS total_of_totals,
  AVG(cost_total) AS avg_of_averages
FROM
  (
    SELECT
      strftime('%m-%Y', date) AS my,
      SUM(cost) AS cost_total
    FROM
      maintenance_entries
    WHERE
      item_id = ?
    GROUP BY
      my
  )`

	row := r.db.Sql().QueryRowContext(ctx, statement, itemID)
	err = row.Scan(&maybeTotal, &maybeAverage)
	if err != nil {
		return MaintenanceLog{}, err
	}

	log.CostAverage = orDefault(maybeAverage, 0)
	log.CostTotal = orDefault(maybeTotal, 0)
	return log, nil
}

func (r *MaintenanceEntryRepository) Delete(ctx context.Context, ID uuid.UUID) error {
	return r.db.MaintenanceEntry.DeleteOneID(ID).Exec(ctx)
}