<script setup lang="ts">
import { currencies, currencyMinorUnits, type CurrencyCode, type EntryType } from "@cents/domain";
import { BookOpen, CalendarDays, Check, ChevronDown, Delete as BackspaceIcon, X } from "@lucide/vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { entryTypes, type Category } from "../data/categories";
import CategoryCoordinateSelector from "./CategoryCoordinateSelector.vue";
import CurrencyPicker from "./CurrencyPicker.vue";

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

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

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

const expression = ref("0");
const currency = ref<CurrencyCode>(props.defaultCurrency);
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 noteEditing = ref(false);
const ledgerPickerOpen = ref(false);
const currencyPickerOpen = ref(false);
const drawerDragging = ref(false);
const drawerDragOffset = ref(0);
const drawerClosing = ref(false);
const drawerEntered = ref(false);
const drawerPointerId = ref<number | null>(null);
let drawerDragStartY = 0;
let drawerCloseTimer: number | null = null;
let drawerEnterFrame: number | null = null;
const selectedLedgerIds = ref([...new Set([props.ledgerId, props.personalLedgerId].filter(Boolean))]);

const typeOption = computed(() => entryTypes.find((item) => item.id === entryType.value)!);
const selectedCategory = computed(() => categoryPath.value.at(-1));
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 minorUnits = computed(() => currencyMinorUnits(currency.value));
const hasCalculation = computed(() => /[+-]/.test(expression.value));
const calculationLabel = computed(() => expression.value.replaceAll("-", "−").replaceAll("+", " + ").replaceAll("−", " − "));
const displayAmount = computed(() =>
  new Intl.NumberFormat("zh-CN", {
    minimumFractionDigits: minorUnits.value,
    maximumFractionDigits: minorUnits.value,
  }).format(calculatedAmount.value),
);
const amountSizeClass = computed(() => {
  const length = displayAmount.value.length;
  if (length <= 8) return "amount-normal";
  if (length <= 10) return "amount-compact";
  if (length <= 13) return "amount-small";
  return "amount-tiny";
});
const amountNeedsFullRow = computed(() => displayAmount.value.length > 10);
const currencySymbol = computed(() => currencies[currency.value].symbol);
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 * 10 ** minorUnits.value);
  return Number.isFinite(amount) && amount > 0 && occurredAtValid.value;
});

watch(currency, () => {
  if (minorUnits.value === 0 && expression.value.includes(".")) {
    expression.value = String(Math.round(calculatedAmount.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;
    const factor = 10 ** minorUnits.value;
    result = Math.round(result * factor) / factor;
  });

  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 ((minorUnits.value > 0 && decimalPlaces >= minorUnits.value) || 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() {
  if (minorUnits.value === 0) return;
  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;
}

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

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

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

function finishNoteEditing(event: KeyboardEvent) {
  if (event.isComposing) return;
  event.preventDefault();
  noteEditing.value = false;
  (event.currentTarget as HTMLInputElement).blur();
}

function startDrawerDrag(event: PointerEvent) {
  if (drawerClosing.value || (event.pointerType === "mouse" && event.button !== 0)) return;
  drawerPointerId.value = event.pointerId;
  drawerDragStartY = event.clientY;
  drawerDragOffset.value = 0;
  drawerDragging.value = true;
  (event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId);
}

function moveDrawerDrag(event: PointerEvent) {
  if (drawerPointerId.value !== event.pointerId) return;
  drawerDragOffset.value = Math.max(0, Math.min(window.innerHeight * 0.82, event.clientY - drawerDragStartY));
}

function finishDrawerDrag(event: PointerEvent) {
  if (drawerPointerId.value !== event.pointerId) return;
  drawerPointerId.value = null;
  if (drawerDragOffset.value >= 56) {
    requestClose();
    return;
  }
  drawerDragging.value = false;
  drawerDragOffset.value = 0;
}

function cancelDrawerDrag(event: PointerEvent) {
  if (drawerPointerId.value !== event.pointerId) return;
  drawerPointerId.value = null;
  drawerDragging.value = false;
  drawerDragOffset.value = 0;
}

function requestClose() {
  if (drawerClosing.value) return;
  drawerClosing.value = true;
  drawerDragging.value = false;
  drawerDragOffset.value = 0;
  drawerCloseTimer = window.setTimeout(() => {
    drawerCloseTimer = null;
    emit("close");
  }, 220);
}

onBeforeUnmount(() => {
  if (drawerCloseTimer !== null) window.clearTimeout(drawerCloseTimer);
  if (drawerEnterFrame !== null) window.cancelAnimationFrame(drawerEnterFrame);
});

onMounted(() => {
  drawerEnterFrame = window.requestAnimationFrame(() => {
    drawerEnterFrame = null;
    drawerEntered.value = true;
  });
});

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

function openCurrencyPicker() {
  confirmCategory();
  noteEditing.value = false;
  currencyPickerOpen.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 * 10 ** minorUnits.value);
  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,
    currency: currency.value,
  });
}
</script>

<template>
  <div class="drawer-scrim" :class="{ 'category-mode': selectorOpen, 'drawer-entered': drawerEntered, 'drawer-closing': drawerClosing }" @click="requestClose"></div>
  <section
    class="quick-drawer"
    :class="{ 'drawer-entered': drawerEntered, 'drawer-dragging': drawerDragging, 'drawer-closing': drawerClosing }"
    :style="drawerDragging ? { transform: `translateY(${drawerDragOffset}px)` } : undefined"
    aria-label="快速记账"
  >
    <div v-if="selectorOpen" class="category-focus-scrim"></div>
    <div
      class="drawer-handle"
      role="button"
      aria-label="向下拖动关闭快速记账"
      title="向下拖动关闭"
      @pointerdown="startDrawerDrag"
      @pointermove="moveDrawerDrag"
      @pointerup="finishDrawerDrag"
      @pointercancel="cancelDrawerDrag"
    ></div>

    <header class="drawer-header" :class="{ 'long-amount': amountNeedsFullRow }">
      <div class="amount-block">
        <div class="amount-result" :class="amountSizeClass">
          <button class="amount-prefix" type="button" :aria-label="`切换币种，当前${currencies[currency].name}`" @click="openCurrencyPicker">
            {{ currencySymbol }}
          </button>
          <strong class="amount-value" :class="amountSizeClass">{{ 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"
          @keydown.enter="finishNoteEditing"
        />
      </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': selectedCategory?.color ?? typeOption.color }"
          :aria-label="`选择分类，当前为${categoryPathLabel}`"
          @click.stop="openSelector"
        >
          <component :is="selectedCategory?.icon ?? typeOption.icon" :size="17" />
          <span>{{ categoryDisplayLabel }}</span>
        </button>

        <CategoryCoordinateSelector
          v-model:entry-type="entryType"
          v-model:category-path="categoryPath"
          :open="selectorOpen"
          @confirm="confirmCategory"
          @close="confirmCategory"
        />
      </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.gradient }"><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>

  <CurrencyPicker v-model="currency" :open="currencyPickerOpen" @close="currencyPickerOpen = false" />
</template>
