<script setup lang="ts">
import type { EntryType } from "@cents/domain";
import { BookOpen, CalendarDays, Check, ChevronDown, ChevronRight, Delete as BackspaceIcon, X } from "@lucide/vue";
import { computed, ref } from "vue";
import {
  categories,
  entryTypes,
  type Category,
  type EntryTypeOption,
} from "../data/categories";

type SavedEntry = {
  type: EntryType;
  amount: number;
  categoryId: string;
  note: string;
  occurredAt: string;
  ledgerIds: string[];
};

type SelectorLevel = "type" | "category" | "subcategory";

const props = defineProps<{
  ledgerId: string;
  ledgerName: string;
  personalLedgerId: string;
  ledgerOptions: Array<{ id: string; name: string; color: string; isPersonal: boolean }>;
}>();

const emit = defineEmits<{
  close: [];
  save: [entry: SavedEntry];
}>();

const expression = ref("0");
const entryType = ref<EntryType>("expense");
const categoryPath = ref<Category[]>([]);
const note = ref("");
const occurredAt = ref(toLocalDateTime(new Date()));
const dateInput = ref<HTMLInputElement | null>(null);
const selectorOpen = ref(false);
const selectorLevel = ref<SelectorLevel>("type");
const noteEditing = ref(false);
const ledgerPickerOpen = ref(false);
const selectedLedgerIds = ref([...new Set([props.ledgerId, props.personalLedgerId].filter(Boolean))]);
const axisOffset = ref(0);
let axisPointerId: number | null = null;
let axisDragStartY = 0;
let axisDragStartOffset = 0;
let axisDragged = false;
let axisLastY = 0;
let axisLastTime = 0;
let axisVelocity = 0;
let axisInertiaFrame: number | null = null;

const typeOption = computed(() => entryTypes.find((item) => item.id === entryType.value)!);
const selectedCategory = computed(() => categoryPath.value.at(-1));
const selectedParent = computed(() => categoryPath.value[0]);
const categoryPathLabel = computed(() =>
  [typeOption.value.label, ...categoryPath.value.map((category) => category.label)].join(">"),
);
const categoryDisplayLabel = computed(() => selectedCategory.value?.label ?? typeOption.value.label);
const calculatedAmount = computed(() => evaluateExpression(expression.value));
const hasCalculation = computed(() => /[+-]/.test(expression.value));
const calculationLabel = computed(() => expression.value.replaceAll("-", "−").replaceAll("+", " + ").replaceAll("−", " − "));
const displayAmount = computed(() =>
  new Intl.NumberFormat("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  }).format(calculatedAmount.value),
);
const selectedLedgerLabel = computed(() => {
  const visibleNames = props.ledgerOptions
    .filter((ledger) => !ledger.isPersonal && selectedLedgerIds.value.includes(ledger.id))
    .map((ledger) => ledger.name);
  if (!visibleNames.length) return "个人账本";
  const names = visibleNames.slice(0, 3).join(",");
  return visibleNames.length > 1 ? `${names} 等 ${visibleNames.length} 个账本` : names;
});
const occurredAtValid = computed(() => Boolean(occurredAt.value) && !Number.isNaN(new Date(occurredAt.value).getTime()));
const dateLabel = computed(() => {
  if (!occurredAtValid.value) return "选择时间";
  const date = new Date(occurredAt.value);
  const now = new Date();
  const datePart = isSameDay(date, now)
    ? "今天"
    : `${date.getMonth() + 1}月${date.getDate()}日`;
  return `${datePart} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
});
const canSave = computed(() => {
  const amount = Math.round(calculatedAmount.value * 100);
  return Number.isFinite(amount) && amount > 0 && occurredAtValid.value;
});
const selectorOptions = computed<(EntryTypeOption | Category)[]>(() => {
  if (selectorLevel.value === "type") {
    const current = entryTypes.find((option) => option.id === entryType.value);
    return [...entryTypes.filter((option) => option.id !== entryType.value), ...(current ? [current] : [])];
  }
  if (selectorLevel.value === "category") return categories[entryType.value];
  return selectedParent.value?.children ?? [];
});
const upperSelectorOptions = computed(() => {
  const count = Math.min(2, Math.floor(selectorOptions.value.length / 2));
  return selectorOptions.value.slice(0, count);
});
const upperAxisHeight = computed(() => {
  const count = upperSelectorOptions.value.length;
  return count * 72 + Math.max(0, count - 1) * 10 + 34;
});
const axisBaseOffset = computed(() => upperSelectorOptions.value.length * 82);
const axisMaxOffset = computed(() =>
  Math.max(0, (selectorOptions.value.length - upperSelectorOptions.value.length - 2) * 82),
);
const selectorPath = computed<(EntryTypeOption | Category)[]>(() => {
  if (selectorLevel.value === "type") return [];
  if (selectorLevel.value === "category") return [typeOption.value];
  return selectedParent.value ? [typeOption.value, selectedParent.value] : [typeOption.value];
});

function toLocalDateTime(date: Date) {
  const year = date.getFullYear();
  const month = pad(date.getMonth() + 1);
  const day = pad(date.getDate());
  const hours = pad(date.getHours());
  const minutes = pad(date.getMinutes());
  return `${year}-${month}-${day}T${hours}:${minutes}`;
}

function pad(value: number) {
  return String(value).padStart(2, "0");
}

function isSameDay(left: Date, right: Date) {
  return (
    left.getFullYear() === right.getFullYear() &&
    left.getMonth() === right.getMonth() &&
    left.getDate() === right.getDate()
  );
}

function evaluateExpression(value: string) {
  const operands = value.split(/[+-]/);
  const operators = value.match(/[+-]/g) ?? [];
  let result = Number(operands[0]) || 0;

  operators.forEach((operator, index) => {
    const operand = operands[index + 1];
    if (operand === "" || operand === ".") return;
    const right = Number(operand);
    result = operator === "+" ? result + right : result - right;
    result = Math.round(result * 100) / 100;
  });

  return result;
}

function currentOperand() {
  return expression.value.split(/[+-]/).at(-1) ?? "";
}

function appendDigit(digit: string) {
  const operand = currentOperand();
  const decimalPlaces = operand.includes(".") ? operand.split(".")[1].length : 0;
  if (decimalPlaces >= 2 || operand.replace(".", "").length >= 9) return;

  if (expression.value === "0") expression.value = digit;
  else if (operand === "0") expression.value = `${expression.value.slice(0, -1)}${digit}`;
  else expression.value += digit;
}

function appendDecimal() {
  const operand = currentOperand();
  if (operand.includes(".")) return;
  expression.value += operand === "" ? "0." : ".";
}

function backspace() {
  expression.value = expression.value.length > 1 ? expression.value.slice(0, -1) : "0";
}

function selectOperator(operator: "+" | "-") {
  if (expression.value.endsWith(".")) expression.value += "0";
  if (/[+-]$/.test(expression.value)) expression.value = `${expression.value.slice(0, -1)}${operator}`;
  else expression.value += operator;
}

function openSelector() {
  if (selectorOpen.value) return;
  selectorOpen.value = true;
  resetAxisScroll();
}

function openDatePicker() {
  confirmCategory();
  const input = dateInput.value;
  if (!input) return;
  if (typeof input.showPicker === "function") input.showPicker();
  else input.click();
}

function chooseSelectorOption(option: EntryTypeOption | Category) {
  if (selectorLevel.value === "type") {
    const typeChanged = option.id !== entryType.value;
    entryType.value = option.id as EntryType;
    if (typeChanged) categoryPath.value = [];
    selectorLevel.value = "category";
    resetAxisScroll();
    return;
  }

  const category = option as Category;
  if (selectorLevel.value === "category") {
    const currentChild = selectedParent.value?.id === category.id ? categoryPath.value[1] : undefined;
    categoryPath.value = currentChild ? [category, currentChild] : [category];
    if (category.children?.length) {
      selectorLevel.value = "subcategory";
      resetAxisScroll();
    }
    else confirmCategory();
    return;
  }

  if (!selectedParent.value) return;
  categoryPath.value = [selectedParent.value, category];
  confirmCategory();
}

function choosePathLevel(index: number) {
  selectorLevel.value = index === 0 ? "type" : "category";
  resetAxisScroll();
}

function isSelectorOptionSelected(option: EntryTypeOption | Category) {
  if (selectorLevel.value === "type") return option.id === entryType.value;
  if (selectorLevel.value === "category") return option.id === selectedParent.value?.id;
  return categoryPath.value.length > 1 && option.id === selectedCategory.value?.id;
}

function resetAxisScroll() {
  stopAxisInertia();
  axisOffset.value = 0;
}

function clampAxisOffset(value: number) {
  return Math.max(0, Math.min(axisMaxOffset.value, value));
}

function startAxisDrag(event: PointerEvent) {
  stopAxisInertia();
  axisPointerId = event.pointerId;
  axisDragStartY = event.clientY;
  axisDragStartOffset = axisOffset.value;
  axisDragged = false;
  axisLastY = event.clientY;
  axisLastTime = performance.now();
  axisVelocity = 0;
  (event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
}

function moveAxisDrag(event: PointerEvent) {
  if (axisPointerId !== event.pointerId) return;
  const distance = axisDragStartY - event.clientY;
  if (Math.abs(distance) > 5) axisDragged = true;
  axisOffset.value = clampAxisOffset(axisDragStartOffset + distance);

  const now = performance.now();
  const elapsed = Math.max(1, now - axisLastTime);
  const instantaneousVelocity = (axisLastY - event.clientY) / elapsed;
  axisVelocity = axisVelocity * 0.65 + instantaneousVelocity * 0.35;
  axisLastY = event.clientY;
  axisLastTime = now;
}

function endAxisDrag(event: PointerEvent) {
  if (axisPointerId !== event.pointerId) return;
  axisPointerId = null;
  const target = event.currentTarget as HTMLElement;
  if (target.hasPointerCapture(event.pointerId)) target.releasePointerCapture(event.pointerId);
  if (axisDragged) startAxisInertia();
}

function handleAxisClick(event: MouseEvent) {
  if (!axisDragged) return;
  event.preventDefault();
  event.stopPropagation();
  axisDragged = false;
}

function scrollAxisBy(delta: number) {
  stopAxisInertia();
  axisOffset.value = clampAxisOffset(axisOffset.value + delta);
}

function startAxisInertia() {
  if (Math.abs(axisVelocity) < 0.04) return;
  let previousTime = performance.now();

  const step = (now: number) => {
    const elapsed = Math.min(32, now - previousTime);
    previousTime = now;
    const current = axisOffset.value;
    const next = clampAxisOffset(current + axisVelocity * elapsed);
    axisOffset.value = next;

    if (next === current && (next === 0 || next === axisMaxOffset.value)) {
      stopAxisInertia();
      return;
    }

    axisVelocity *= Math.pow(0.91, elapsed / 16);
    if (Math.abs(axisVelocity) < 0.025) {
      stopAxisInertia();
      return;
    }
    axisInertiaFrame = requestAnimationFrame(step);
  };

  axisInertiaFrame = requestAnimationFrame(step);
}

function stopAxisInertia() {
  if (axisInertiaFrame !== null) cancelAnimationFrame(axisInertiaFrame);
  axisInertiaFrame = null;
  axisVelocity = 0;
}

function confirmCategory() {
  selectorOpen.value = false;
}

function startNoteEditing() {
  confirmCategory();
  noteEditing.value = true;
}

function openLedgerPicker() {
  confirmCategory();
  noteEditing.value = false;
  ledgerPickerOpen.value = true;
}

function toggleLedger(ledgerId: string) {
  if (ledgerId === props.personalLedgerId) return;
  if (selectedLedgerIds.value.includes(ledgerId)) {
    if (selectedLedgerIds.value.length > 1) {
      selectedLedgerIds.value = selectedLedgerIds.value.filter((id) => id !== ledgerId);
    }
    return;
  }
  selectedLedgerIds.value = [...selectedLedgerIds.value, ledgerId];
}

function saveEntry() {
  const amount = Math.round(calculatedAmount.value * 100);
  if (!canSave.value) return;

  emit("save", {
    type: entryType.value,
    amount,
    categoryId: selectedCategory.value?.id ?? entryType.value,
    note: note.value.trim(),
    occurredAt: new Date(occurredAt.value).toISOString(),
    ledgerIds: selectedLedgerIds.value,
  });
}
</script>

<template>
  <div class="drawer-scrim" :class="{ 'category-mode': selectorOpen }" @click="emit('close')"></div>
  <section class="quick-drawer" aria-label="快速记账">
    <div v-if="selectorOpen" class="category-focus-scrim" @click="confirmCategory"></div>
    <div class="drawer-handle"></div>

    <header class="drawer-header">
      <div class="amount-block">
        <div class="amount-result">
          <span class="amount-prefix">¥</span>
          <strong class="amount-value">{{ displayAmount }}</strong>
        </div>
        <span v-if="hasCalculation" class="calculation-line">{{ calculationLabel }}</span>
      </div>
      <button class="quick-ledger-target" type="button" @click="openLedgerPicker">
        <BookOpen :size="14" />
        <span>记入 {{ selectedLedgerLabel }}</span>
        <ChevronDown :size="13" />
      </button>
    </header>

    <div class="entry-meta-row">
      <label class="note-field" :class="{ editing: noteEditing }">
        <input
          v-model="note"
          maxlength="40"
          placeholder="备注"
          aria-label="备注"
          @focus="startNoteEditing"
          @blur="noteEditing = false"
        />
      </label>

      <button class="header-date-button" :class="{ invalid: !occurredAtValid }" type="button" :aria-invalid="!occurredAtValid" @click="openDatePicker">
        <CalendarDays :size="15" />
        <span>{{ dateLabel }}</span>
      </button>
      <input
        ref="dateInput"
        v-model="occurredAt"
        class="native-date-input"
        type="datetime-local"
        aria-label="发生时间"
        tabindex="-1"
      />

      <div class="category-selector-anchor" :class="{ 'selector-open': selectorOpen }">
        <button
          type="button"
          class="meta-category-button"
          :class="{ active: selectorOpen }"
          :style="{ '--category-color': selectorOpen ? '#087f72' : selectedCategory?.color ?? typeOption.color }"
          :aria-label="selectorOpen ? '确认当前分类' : `选择分类，当前为${categoryPathLabel}`"
          @click.stop="selectorOpen ? confirmCategory() : openSelector()"
        >
          <Check v-if="selectorOpen" :size="22" />
          <component v-else :is="selectedCategory?.icon ?? typeOption.icon" :size="17" />
          <span>{{ selectorOpen ? "确定" : categoryDisplayLabel }}</span>
        </button>

        <Transition name="coordinate-selector">
          <div v-if="selectorOpen" class="category-coordinate" aria-label="二维分类选择器" @click.stop>
            <div class="selector-path" aria-label="已选分类路径">
              <template v-for="(option, index) in selectorPath" :key="option.id">
                <ChevronRight v-if="index > 0" :size="18" aria-hidden="true" />
                <button
                  type="button"
                  :style="{ '--path-color': option.color }"
                  :aria-label="`返回${option.label}层重新选择`"
                  @click="choosePathLevel(index)"
                >
                  <component :is="option.icon" :size="22" />
                  <small>{{ option.label }}</small>
                </button>
              </template>
              <ChevronRight v-if="selectorPath.length" :size="18" aria-hidden="true" />
            </div>

            <div
              v-if="upperSelectorOptions.length"
              class="candidate-above-axis"
              :style="{ height: `${upperAxisHeight}px` }"
            >
              <div
                class="candidate-axis-viewport"
                @pointerdown="startAxisDrag"
                @pointermove="moveAxisDrag"
                @pointerup="endAxisDrag"
                @pointercancel="endAxisDrag"
                @click.capture="handleAxisClick"
                @wheel.prevent="scrollAxisBy($event.deltaY)"
              >
                <div class="candidate-axis-list" :style="{ transform: `translate3d(0, -${axisOffset}px, 0)` }">
                  <button
                    v-for="option in selectorOptions"
                    :key="option.id"
                    type="button"
                    class="coordinate-option"
                    :class="{ selected: isSelectorOptionSelected(option) }"
                    :style="{ '--option-color': option.color, '--option-tint': option.tint }"
                    @click="chooseSelectorOption(option)"
                  >
                    <span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
                    <span>{{ option.label }}</span>
                  </button>
                </div>
              </div>
            </div>

            <div class="candidate-below-axis">
              <div
                class="candidate-axis-viewport"
                @pointerdown="startAxisDrag"
                @pointermove="moveAxisDrag"
                @pointerup="endAxisDrag"
                @pointercancel="endAxisDrag"
                @click.capture="handleAxisClick"
                @wheel.prevent="scrollAxisBy($event.deltaY)"
              >
                <div
                  class="candidate-axis-list"
                  :style="{ transform: `translate3d(0, -${axisBaseOffset + axisOffset}px, 0)` }"
                >
                  <button
                    v-for="option in selectorOptions"
                    :key="option.id"
                    type="button"
                    class="coordinate-option"
                    :class="{ selected: isSelectorOptionSelected(option) }"
                    :style="{ '--option-color': option.color, '--option-tint': option.tint }"
                    @click="chooseSelectorOption(option)"
                  >
                    <span class="coordinate-icon"><component :is="option.icon" :size="24" /></span>
                    <span>{{ option.label }}</span>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </Transition>
      </div>
    </div>

    <div v-if="!noteEditing" class="keypad">
      <button type="button" class="key number-key" @click="appendDigit('7')">7</button>
      <button type="button" class="key number-key" @click="appendDigit('8')">8</button>
      <button type="button" class="key number-key" @click="appendDigit('9')">9</button>
      <button type="button" class="key operator-key" :class="{ active: expression.endsWith('+') }" @click="selectOperator('+')">+</button>

      <button type="button" class="key number-key" @click="appendDigit('4')">4</button>
      <button type="button" class="key number-key" @click="appendDigit('5')">5</button>
      <button type="button" class="key number-key" @click="appendDigit('6')">6</button>
      <button type="button" class="key operator-key" :class="{ active: expression.endsWith('-') }" @click="selectOperator('-')">−</button>

      <button type="button" class="key number-key" @click="appendDigit('1')">1</button>
      <button type="button" class="key number-key" @click="appendDigit('2')">2</button>
      <button type="button" class="key number-key" @click="appendDigit('3')">3</button>
      <button type="button" class="key save-key" :disabled="!canSave" aria-label="完成记账" title="完成记账" @click="saveEntry">
        <Check :size="27" />
      </button>

      <button type="button" class="key number-key" aria-label="小数点" @click="appendDecimal">.</button>
      <button type="button" class="key number-key" @click="appendDigit('0')">0</button>
      <button type="button" class="key utility-key" aria-label="退格" title="退格" @click="backspace">
        <BackspaceIcon :size="22" />
      </button>
    </div>
  </section>

  <div v-if="ledgerPickerOpen" class="quick-ledger-picker-layer">
    <button class="quick-ledger-picker-scrim" type="button" aria-label="关闭账本选择" @click="ledgerPickerOpen = false"></button>
    <section class="quick-ledger-picker" aria-label="选择关联账本">
      <div class="quick-ledger-picker-handle"></div>
      <header><div><strong>选择账本</strong><span>至少选择一个账本</span></div><button type="button" aria-label="关闭" @click="ledgerPickerOpen = false"><X :size="19" /></button></header>
      <div class="quick-ledger-options">
        <button v-for="ledger in ledgerOptions" :key="ledger.id" type="button" :class="{ selected: selectedLedgerIds.includes(ledger.id), automatic: ledger.isPersonal }" :disabled="ledger.isPersonal" @click="toggleLedger(ledger.id)">
          <span :style="{ background: ledger.color }"><BookOpen :size="17" /></span>
          <strong>{{ ledger.name }}<small v-if="ledger.isPersonal">自动记入</small></strong>
          <Check v-if="selectedLedgerIds.includes(ledger.id)" :size="19" />
        </button>
      </div>
      <button class="quick-ledger-picker-done" type="button" @click="ledgerPickerOpen = false"><Check :size="18" />完成</button>
    </section>
  </div>
</template>
