<!--
This page is quite large, and handles the form in which users can create and
edit transactions. It's accessed through two routes:
- /profiles/:profileName/transactions/:transactionId for editing
- /profiles/:profileName/add-transaction for creating a new transaction

The form consists of a few main sections:
- Standard form controls for various fields like timestamp, amount, description, etc.
- Line items table for editing the list of line items.
- Tags editor for editing the set of tags.
-->
<script setup lang="ts">
import { AccountApiClient, type Account } from '@/api/account'
import { DataApiClient, floatMoneyToInteger, type Currency } from '@/api/data'
import { getSelectedProfile } from '@/api/profile'
import {
  TransactionApiClient,
  type AddTransactionPayload,
  type TransactionDetail,
  type TransactionLineItemResponse,
  type TransactionVendor,
} from '@/api/transaction'
import AppPage from '@/components/common/AppPage.vue'
import CategorySelect from '@/components/CategorySelect.vue'
import FileSelector from '@/components/common/FileSelector.vue'
import AppForm from '@/components/common/form/AppForm.vue'
import FormActions from '@/components/common/form/FormActions.vue'
import FormControl from '@/components/common/form/FormControl.vue'
import FormGroup from '@/components/common/form/FormGroup.vue'
import LineItemsEditor from '@/components/LineItemsEditor.vue'
import { getDatetimeLocalValueForNow } from '@/util/time'
import { computed, onMounted, ref, watch, type Ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import VendorSelect from '@/components/VendorSelect.vue'
import TagsSelect from '@/components/TagsSelect.vue'

const route = useRoute()
const router = useRouter()

const transactionApi = new TransactionApiClient(getSelectedProfile(route))
const accountApi = new AccountApiClient(route)

const existingTransaction: Ref<TransactionDetail | null> = ref(null)
const editing = computed(() => {
  return existingTransaction.value !== null || route.meta.title === 'Edit Transaction'
})
const formValid = computed(() => {
  return (
    timestamp.value.length > 0 &&
    amount.value > 0 &&
    currency.value !== null &&
    description.value.length > 0 &&
    (creditedAccountId.value !== null || debitedAccountId.value !== null) &&
    creditedAccountId.value !== debitedAccountId.value
  )
})
const unsavedEdits = computed(() => {
  console.log('Checking if there are unsaved edits...')
  if (!existingTransaction.value) return true
  const tx = existingTransaction.value
  const tagsEqual =
    tags.value.every((t) => tx.tags.includes(t)) && tx.tags.every((t) => tags.value.includes(t))
  let lineItemsEqual = false
  if (lineItems.value.length === tx.lineItems.length) {
    lineItemsEqual = true
    for (let i = 0; i < lineItems.value.length; i++) {
      const i1 = lineItems.value[i]
      const i2 = tx.lineItems[i]
      if (
        i1.idx !== i2.idx ||
        i1.quantity !== i2.quantity ||
        i1.valuePerItem !== i2.valuePerItem ||
        i1.description !== i2.description ||
        (i1.category?.id ?? null) !== (i2.category?.id ?? null)
      ) {
        lineItemsEqual = false
        break
      }
    }
  }
  const attachmentsChanged =
    attachmentsToUpload.value.length > 0 || removedAttachmentIds.value.length > 0
  const timestampChanged = new Date(timestamp.value).toISOString() !== tx.timestamp
  const amountChanged =
    amount.value * Math.pow(10, currency.value?.fractionalDigits ?? 0) !== tx.amount
  const currencyChanged = currency.value?.code !== tx.currency.code
  const descriptionChanged = description.value !== tx.description
  const internalTransferChanged = internalTransfer.value !== tx.internalTransfer
  const vendorChanged = vendor.value?.id !== tx.vendor?.id
  const categoryChanged = categoryId.value !== (tx.category?.id ?? null)
  const creditedAccountChanged = creditedAccountId.value !== (tx.creditedAccount?.id ?? null)
  const debitedAccountChanged = debitedAccountId.value !== (tx.debitedAccount?.id ?? null)
  console.log(`
    Timestamp changed: ${timestampChanged}
    Amount changed:    ${amountChanged}
    Currency changed:  ${currencyChanged}
    Description changed: ${descriptionChanged}
    Internal Transfer changed: ${internalTransferChanged}
    Vendor changed: ${vendorChanged}
    Category changed: ${categoryChanged}
    Credited account changed: ${creditedAccountChanged}
    Debited account changed: ${debitedAccountChanged}
    Tags changed: ${!tagsEqual}
    Line items changed: ${!lineItemsEqual}
    Attachments changed: ${attachmentsChanged}
  `)

  return (
    timestampChanged ||
    amountChanged ||
    currencyChanged ||
    descriptionChanged ||
    internalTransferChanged ||
    vendorChanged ||
    categoryChanged ||
    creditedAccountChanged ||
    debitedAccountChanged ||
    !tagsEqual ||
    !lineItemsEqual ||
    attachmentsChanged
  )
})

// General data used to populate form controls.
const allCurrencies: Ref<Currency[]> = ref([])
const availableCurrencies = computed(() => {
  return allCurrencies.value.filter((c) =>
    allAccounts.value.some((a) => a.currency.code === c.code),
  )
})
const allAccounts: Ref<Account[]> = ref([])
const availableAccounts = computed(() => {
  return allAccounts.value.filter((a) => a.currency.code === currency.value?.code)
})
const loading = ref(false)

// Form data:
const timestamp = ref('')
const amount = ref(0)
const currency: Ref<Currency | null> = ref(null)
const description = ref('')
const internalTransfer = ref(false)
const vendor: Ref<TransactionVendor | null> = ref(null)
const categoryId: Ref<number | null> = ref(null)
const creditedAccountId: Ref<number | null> = ref(null)
const debitedAccountId: Ref<number | null> = ref(null)
const lineItems: Ref<TransactionLineItemResponse[]> = ref([])
const tags: Ref<string[]> = ref([])
const customTagInput = ref('')
const customTagInputValid = ref(false)
const attachmentsToUpload: Ref<File[]> = ref([])
const removedAttachmentIds: Ref<number[]> = ref([])

watch(customTagInput, (newValue: string) => {
  const result = newValue.match('^[a-z0-9-_]{3,32}$')
  customTagInputValid.value = result !== null && result.length > 0
})
watch(availableCurrencies, (newValue: Currency[]) => {
  if (newValue.length === 1) {
    currency.value = newValue[0]
  }
})

onMounted(async () => {
  const dataClient = new DataApiClient()

  // Fetch various collections of data needed for different user choices.
  dataClient.getCurrencies().then((currencies) => (allCurrencies.value = currencies))
  accountApi.getAccounts().then((accounts) => (allAccounts.value = accounts))

  const transactionIdStr = route.params.id
  if (transactionIdStr && typeof transactionIdStr === 'string') {
    const transactionId = parseInt(transactionIdStr)
    try {
      loading.value = true
      existingTransaction.value = await transactionApi.getTransaction(transactionId)
      loadValuesFromExistingTransaction(existingTransaction.value)
    } catch (err) {
      console.error(err)
    } finally {
      loading.value = false
    }
  } else {
    // Load default values.
    timestamp.value = getDatetimeLocalValueForNow()
    amount.value = Math.pow(10, currency.value?.fractionalDigits ?? 0)
  }

  // Load default values from the query parameters.
  if ('credited-account' in route.query) {
    creditedAccountId.value = parseInt(route.query['credited-account'] as string)
  }
  if ('debited-account' in route.query) {
    debitedAccountId.value = parseInt(route.query['debited-account'] as string)
  }
})

/**
 * Submits the transaction. If the user is editing an existing transaction,
 * then that transaction will be updated. Otherwise, a new transaction is
 * created.
 */
async function doSubmit() {
  if (currency.value === null) {
    return
  }

  let vendorId: number | null = vendor.value?.id ?? null
  if (vendor.value !== null && vendorId === -1) {
    const newVendor = await transactionApi.createVendor({
      name: vendor.value?.name,
      description: null,
    })
    vendorId = newVendor.id
  }

  const localDate = new Date(timestamp.value)
  const payload: AddTransactionPayload = {
    timestamp: localDate.toISOString(),
    amount: floatMoneyToInteger(amount.value, currency.value),
    currencyCode: currency.value?.code ?? '',
    description: description.value,
    internalTransfer: internalTransfer.value,
    vendorId: vendorId,
    categoryId: categoryId.value,
    creditedAccountId: creditedAccountId.value,
    debitedAccountId: debitedAccountId.value,
    tags: tags.value,
    lineItems: lineItems.value.map((i) => {
      return { ...i, categoryId: i.category?.id ?? null }
    }),
    attachmentIdsToRemove: removedAttachmentIds.value,
  }

  let savedTransaction = null
  try {
    loading.value = true
    if (existingTransaction.value) {
      savedTransaction = await transactionApi.updateTransaction(
        existingTransaction.value?.id,
        payload,
        attachmentsToUpload.value,
      )
    } else {
      savedTransaction = await transactionApi.addTransaction(payload, attachmentsToUpload.value)
    }
    await router.replace(
      `/profiles/${getSelectedProfile(route)}/transactions/${savedTransaction.id}`,
    )
  } catch (err) {
    console.error(err)
  } finally {
    loading.value = false
  }
}

/**
 * Cancels editing / submitting a transaction, and takes the user back to their
 * profile's homepage.
 */
function doCancel() {
  if (editing.value) {
    router.replace(
      `/profiles/${getSelectedProfile(route)}/transactions/${existingTransaction.value?.id}`,
    )
  } else {
    router.replace(`/profiles/${getSelectedProfile(route)}`)
  }
}

function loadValuesFromExistingTransaction(t: TransactionDetail) {
  timestamp.value = getLocalDateTimeStringFromUTCTimestamp(t.timestamp)
  amount.value = t.amount / Math.pow(10, t.currency.fractionalDigits)
  currency.value = t.currency
  description.value = t.description
  internalTransfer.value = t.internalTransfer
  vendor.value = t.vendor ?? null
  categoryId.value = t.category?.id ?? null
  creditedAccountId.value = t.creditedAccount?.id ?? null
  debitedAccountId.value = t.debitedAccount?.id ?? null
  lineItems.value = [...t.lineItems]
  tags.value = [...t.tags]
}

function getLocalDateTimeStringFromUTCTimestamp(timestamp: string) {
  const date = new Date(timestamp)
  date.setMilliseconds(0)
  const timezoneOffset = new Date().getTimezoneOffset() * 60_000
  return new Date(date.getTime() - timezoneOffset).toISOString().slice(0, -1)
}
</script>
<template>
  <AppPage :title="editing ? 'Edit Transaction' : 'Add Transaction'">
    <AppForm @submit="doSubmit()">
      <FormGroup>
        <!-- Basic properties -->
        <FormControl label="Timestamp">
          <input
            type="datetime-local"
            v-model="timestamp"
            step="1"
            :disabled="loading"
            style="min-width: 250px"
          />
        </FormControl>
        <FormControl label="Amount">
          <input
            type="number"
            v-model="amount"
            step="0.01"
            min="0.01"
            :disabled="loading"
            style="max-width: 100px"
          />
        </FormControl>
        <FormControl label="Currency">
          <select
            v-model="currency"
            :disabled="loading || availableCurrencies.length === 1"
          >
            <option
              v-for="currency in availableCurrencies"
              :key="currency.code"
              :value="currency"
            >
              {{ currency.code }}
            </option>
          </select>
        </FormControl>
        <FormControl
          label="Description"
          style="min-width: 200px"
        >
          <textarea
            v-model="description"
            :disabled="loading"
          ></textarea>
        </FormControl>
      </FormGroup>

      <FormGroup>
        <!-- Vendor & Category -->
        <FormControl label="Vendor">
          <VendorSelect v-model="vendor" />
        </FormControl>
        <FormControl label="Category">
          <CategorySelect v-model="categoryId" />
        </FormControl>
      </FormGroup>

      <FormGroup>
        <!-- Accounts -->
        <FormControl label="Credited Account">
          <select
            v-model="creditedAccountId"
            :disabled="loading"
          >
            <option
              v-for="account in availableAccounts"
              :key="account.id"
              :value="account.id"
            >
              {{ account.name }} ({{ account.numberSuffix }})
            </option>
            <option :value="null">None</option>
          </select>
        </FormControl>
        <FormControl label="Debited Account">
          <select
            v-model="debitedAccountId"
            :disabled="loading"
          >
            <option
              v-for="account in availableAccounts"
              :key="account.id"
              :value="account.id"
            >
              {{ account.name }} ({{ account.numberSuffix }})
            </option>
            <option :value="null">None</option>
          </select>
        </FormControl>
      </FormGroup>

      <LineItemsEditor
        v-if="currency"
        v-model="lineItems"
        :currency="currency"
        :transaction-amount="floatMoneyToInteger(amount, currency)"
      />

      <FormGroup>
        <!-- Tags -->
        <FormControl label="Tags">
          <TagsSelect v-model="tags" />
        </FormControl>
      </FormGroup>

      <FormGroup>
        <h5>Attachments</h5>
        <FileSelector
          :initial-files="existingTransaction?.attachments ?? []"
          v-model:uploaded-files="attachmentsToUpload"
          v-model:removed-files="removedAttachmentIds"
        />
      </FormGroup>

      <!-- One last group for less-often used fields: -->
      <FormGroup>
        <FormControl
          label="Internal Transfer"
          hint="Mark this transaction as an internal transfer to ignore it in analytics. Useful for things like credit card payments."
        >
          <input
            type="checkbox"
            v-model="internalTransfer"
            :disabled="loading"
          />
        </FormControl>
      </FormGroup>

      <FormActions
        @cancel="doCancel()"
        :disabled="loading || !formValid || !unsavedEdits"
        :submit-text="editing ? 'Save' : 'Add'"
      />
    </AppForm>
  </AppPage>
</template>
