<script setup lang="ts">
import { type Account, AccountApiClient } from '@/api/account'
import { formatMoney } from '@/api/data'
import { defaultPage, type Page, type PageRequest, type SortDir } from '@/api/pagination'
import { getSelectedProfile } from '@/api/profile'
import {
  TransactionApiClient,
  type AggregateTransactionData,
  type TransactionCategory,
  type TransactionVendor,
  type TransactionsListItem,
} from '@/api/transaction'
import AppBadge from '@/components/common/AppBadge.vue'
import AppButton from '@/components/common/AppButton.vue'
import AppPage from '@/components/common/AppPage.vue'
import ButtonBar from '@/components/common/ButtonBar.vue'
import AppForm from '@/components/common/form/AppForm.vue'
import FormControl from '@/components/common/form/FormControl.vue'
import FormGroup from '@/components/common/form/FormGroup.vue'
import PaginationControls from '@/components/common/PaginationControls.vue'
import TransactionCard from '@/components/TransactionCard.vue'
import { computed, onMounted, ref, watch, type Ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import VueSelect from 'vue3-select-component'

interface SortOption {
  label: string
  property: string
}
const SORT_PROPERTIES: SortOption[] = [
  { label: 'Timestamp', property: 'txn.timestamp' },
  { label: 'Added at', property: 'txn.added_at' },
  { label: 'Amount', property: 'txn.amount' },
]

const FETCH_DEBOUNCE_DELAY = 300

const router = useRouter()
const route = useRoute()
const page: Ref<Page<TransactionsListItem>> = ref(defaultPage())
const aggregateData: Ref<AggregateTransactionData | undefined> = ref()
const lastFetchTime = ref(0)
const fetchTimeoutId = ref<number | undefined>(undefined)

// Sorting options:
const selectedSort: Ref<string> = ref(SORT_PROPERTIES[0].property)
const selectedSortDir: Ref<SortDir> = ref('DESC')

// Filtering options:
const searchQuery = ref('')

const tagFilters: Ref<string[]> = ref([])
const availableTags: Ref<string[]> = ref([])
const tagOptions = computed(() => {
  return availableTags.value.map((tag) => {
    return { label: tag, value: tag }
  })
})

const vendorFilters = ref<number[]>([])
const availableVendors = ref<TransactionVendor[]>([])
const vendorOptions = computed(() => {
  return availableVendors.value.map((vendor) => {
    return { label: vendor.name, value: vendor.id }
  })
})

const categoryFilters = ref<number[]>([])
const availableCategories = ref<TransactionCategory[]>([])
const categoryOptions = computed(() =>
  availableCategories.value.map((category) => {
    return { label: category.name, value: category.id }
  }),
)

const accountFilters = ref<number[]>([])
const availableAccounts = ref<Account[]>([])
const accountOptions = computed(() =>
  availableAccounts.value.map((acc) => {
    return { label: `${acc.name} - #${acc.numberSuffix}`, value: acc.id }
  }),
)

const minAmountFilter = ref<number | undefined>(undefined)
const maxAmountFilter = ref<number | undefined>(undefined)

onMounted(async () => {
  loadFiltersFromRoute()
  const api = new TransactionApiClient(getSelectedProfile(route))
  api.getAllTags().then((tags) => (availableTags.value = tags))
  api.getVendors().then((vendors) => (availableVendors.value = vendors))
  api.getCategoriesFlattened().then((categories) => (availableCategories.value = categories))
  const accountApi = new AccountApiClient(route)
  accountApi.getAccounts().then((accounts) => (availableAccounts.value = accounts))
  await fetchPage(1, 10)

  watch(
    [
      selectedSort,
      selectedSortDir,
      searchQuery,
      tagFilters,
      vendorFilters,
      categoryFilters,
      accountFilters,
      minAmountFilter,
      maxAmountFilter,
    ],
    () => {
      window.clearTimeout(fetchTimeoutId.value)
      fetchTimeoutId.value = window.setTimeout(() => {
        fetchPage(1, 10)
      }, FETCH_DEBOUNCE_DELAY)
    },
  )
})

/**
 * Fetches a page of results based on the currently-configured filters, and
 * once a successful result is obtained, the current route is updated to
 * include the latest filters.
 * @param pg The page number to fetch.
 * @param size The number of items in the page.
 */
async function fetchPage(pg: number, size: number) {
  const pageRequest: PageRequest = {
    page: pg,
    size,
    sorts: [
      {
        attribute: selectedSort.value,
        dir: selectedSortDir.value,
      },
    ],
  }
  const params = buildFiltersQuery()
  const urlWithParams = params.size == 0 ? route.path : route.path + '?' + params.toString()
  const api = new TransactionApiClient(getSelectedProfile(route))
  try {
    // Update aggregate data (asynchronously from main fetching).
    aggregateData.value = undefined
    api.getAggregateTransactionData(params).then((data) => (aggregateData.value = data))
    // Fetch the main page of transactions.
    const startTime = performance.now()
    page.value = await api.searchTransactions(params, pageRequest)
    const endTime = performance.now()
    lastFetchTime.value = endTime - startTime
    await router.replace(urlWithParams)
  } catch (err) {
    console.error(err)
    page.value = defaultPage()
  }
}

function buildFiltersQuery(): URLSearchParams {
  const p = new URLSearchParams()
  if (searchQuery.value.trim().length > 0) {
    p.append('q', searchQuery.value.trim())
  }
  for (const tag of tagFilters.value) {
    p.append('tag', tag)
  }
  for (const vendorId of vendorFilters.value) {
    p.append('vendor', vendorId + '')
  }
  for (const categoryId of categoryFilters.value) {
    p.append('category', categoryId + '')
  }
  for (const accountId of accountFilters.value) {
    p.append('account', accountId + '')
  }
  if (minAmountFilter.value !== undefined && minAmountFilter.value > 0) {
    p.append('min-amount', minAmountFilter.value * 100 + '')
  }
  if (maxAmountFilter.value !== undefined && maxAmountFilter.value > 0) {
    p.append('max-amount', maxAmountFilter.value * 100 + '')
  }
  return p
}

function clearFilters() {
  searchQuery.value = ''
  tagFilters.value = []
  vendorFilters.value = []
  categoryFilters.value = []
  accountFilters.value = []
  minAmountFilter.value = undefined
  maxAmountFilter.value = undefined
}

async function exportToFile() {
  const params = buildFiltersQuery()
  params.append('file-format', 'text/csv')
  const api = new TransactionApiClient(getSelectedProfile(route))
  await api.exportTransactions(params)
}

function goToHome() {
  router.push(`/profiles/${getSelectedProfile(route)}`)
}

function loadFiltersFromRoute() {
  searchQuery.value = loadFirstParamValue('q') ?? ''
  tagFilters.value = loadAllParamValues('tag')
  vendorFilters.value = loadAllParamValues('vendor').map((s) => parseInt(s))
  categoryFilters.value = loadAllParamValues('category').map((s) => parseInt(s))
  accountFilters.value = loadAllParamValues('account').map((s) => parseInt(s))
  const minAmount = loadFirstParamValue('min-amount')
  if (minAmount !== undefined) {
    minAmountFilter.value = Math.round(parseInt(minAmount) / 100)
  }
  const maxAmount = loadFirstParamValue('max-amount')
  if (maxAmount !== undefined) {
    maxAmountFilter.value = Math.round(parseInt(maxAmount) / 100)
  }
}

function loadFirstParamValue(key: string): string | undefined {
  if (key in route.query) {
    if (Array.isArray(route.query[key]) && route.query[key][0] !== null) {
      return route.query[key][0]
    } else if (typeof route.query[key] === 'string') {
      return route.query[key]
    }
  }
}

function loadAllParamValues(key: string): string[] {
  if (key in route.query) {
    if (Array.isArray(route.query[key])) {
      return route.query[key].filter((s) => s !== null)
    } else if (route.query[key] !== null) {
      return [route.query[key]]
    }
  }
  return []
}
</script>
<template>
  <AppPage title="Transactions">
    <AppForm>
      <FormGroup>
        <FormControl
          label="Search"
          hint="Free-form text search against description, tags, vendor, category, account."
        >
          <input
            v-model="searchQuery"
            type="text"
            placeholder="Search for transactions..."
          />
        </FormControl>
      </FormGroup>
      <FormGroup>
        <div class="vueselect-control">
          <h5>Tag</h5>
          <VueSelect
            v-model="tagFilters"
            :options="tagOptions"
            placeholder="Select tags"
            is-multi
          />
        </div>
        <div class="vueselect-control">
          <h5>Vendor</h5>
          <VueSelect
            v-model="vendorFilters"
            :options="vendorOptions"
            placeholder="Select vendors"
            is-multi
          />
        </div>
        <div class="vueselect-control">
          <h5>Category</h5>
          <VueSelect
            v-model="categoryFilters"
            :options="categoryOptions"
            placeholder="Select categories"
            is-multi
          />
        </div>
        <div class="vueselect-control">
          <h5>Account</h5>
          <VueSelect
            v-model="accountFilters"
            :options="accountOptions"
            placeholder="Select accounts"
            is-multi
          />
        </div>
      </FormGroup>

      <FormGroup>
        <FormControl label="Max Amount">
          <input
            v-model="maxAmountFilter"
            type="number"
            min="0"
            step="1"
          />
        </FormControl>
        <FormControl label="Min Amount">
          <input
            v-model="minAmountFilter"
            type="number"
            min="0"
            step="1"
          />
        </FormControl>
      </FormGroup>

      <FormGroup>
        <FormControl label="Sort By">
          <select v-model="selectedSort">
            <option
              v-for="sortOpt in SORT_PROPERTIES"
              :key="sortOpt.property"
              :value="sortOpt.property"
            >
              {{ sortOpt.label }}
            </option>
          </select>
        </FormControl>
        <FormControl label="Sort Direction">
          <select v-model="selectedSortDir">
            <option value="DESC">Descending</option>
            <option value="ASC">Ascending</option>
          </select>
        </FormControl>
      </FormGroup>
      <ButtonBar>
        <AppButton
          size="sm"
          icon="home"
          @click="goToHome()"
          >Back to Homepage</AppButton
        >
        <AppButton
          size="sm"
          icon="trash"
          @click="clearFilters()"
          >Clear Filters</AppButton
        >
        <AppButton
          size="sm"
          icon="file-export"
          @click="exportToFile()"
          >Export to CSV</AppButton
        >
      </ButtonBar>
    </AppForm>

    <PaginationControls
      :page="page"
      @update="(pr) => fetchPage(pr.page, pr.size)"
      class="align-right"
    />
    <div>
      <AppBadge size="sm">
        {{ page.totalElements }} search
        {{ page.totalElements == 1 ? 'result' : 'results' }}
        in {{ lastFetchTime }} milliseconds
      </AppBadge>
    </div>
    <div v-if="aggregateData">
      <div
        v-for="data in aggregateData.currencies"
        :key="data.currency.code"
        class="mt-1 mb-1"
      >
        <AppBadge size="sm">
          {{ data.currency.code }} Credits:
          <span class="font-mono text-muted">{{ formatMoney(data.credits, data.currency) }}</span>
        </AppBadge>
        <AppBadge size="sm">
          {{ data.currency.code }} Debits:
          <span class="font-mono text-muted">{{ formatMoney(data.debits, data.currency) }}</span>
        </AppBadge>
        <AppBadge size="sm">
          {{ data.currency.code }} Total:
          <span
            class="font-mono"
            :class="{
              'text-positive': data.balance > 0,
              'text-negative': data.balance < 0,
            }"
          >
            {{ formatMoney(data.balance, data.currency) }}
          </span>
        </AppBadge>
      </div>
    </div>
    <TransactionCard
      v-for="txn in page.items"
      :key="txn.id"
      :tx="txn"
    />
  </AppPage>
</template>
<style lang="css" scoped>
.vueselect-control {
  flex-grow: 1;
  margin: 0.5rem;
}

.vueselect-control > h5 {
  font-size: 0.9rem;
  font-weight: 700;
  margin: 0;
}
</style>
