import type { Attachment } from '@/api/attachment'
import { floatMoneyToInteger, integerMoneyToFloat, type Currency } from '@/api/data'
import { getSelectedProfile } from '@/api/profile'
import {
  TransactionApiClient,
  type AddTransactionPayload,
  type TransactionDetail,
  type TransactionDraftPayload,
  type TransactionDraftResponse,
  type TransactionLineItemResponse,
  type TransactionVendor,
} from '@/api/transaction'
import { getDatetimeLocalValueForNow } from '@/util/time'
import type { RouteLocation, Router } from 'vue-router'

/**
 * The set of all form fields on the transaction editor page. Note that some
 * fields may only be used in certain contexts.
 */
export interface TransactionEditorFormFields {
  timestamp: string | null
  amount: number | null
  templateName: string | null // Only for drafts, not transactions.
  currency: Currency | null
  description: string | null
  internalTransfer: boolean | null
  vendor: TransactionVendor | null
  categoryId: number | null
  creditedAccountId: number | null
  debitedAccountId: number | null
  lineItems: TransactionLineItemResponse[]
  tags: string[]
  attachmentsToUpload: File[]
  removedAttachmentIds: number[]
  existingAttachments: Attachment[]
}

export function defaultEmptyFormFields(): TransactionEditorFormFields {
  return {
    timestamp: null,
    amount: null,
    templateName: null,
    currency: null,
    description: null,
    internalTransfer: null,
    vendor: null,
    categoryId: null,
    creditedAccountId: null,
    debitedAccountId: null,
    lineItems: [],
    tags: [],
    attachmentsToUpload: [],
    removedAttachmentIds: [],
    existingAttachments: [],
  }
}

export interface TransactionEditorAction {
  name: string
  disabled: boolean
  callback: (
    formData: TransactionEditorFormFields,
    route: RouteLocation,
    router: Router,
  ) => Promise<void>
}

/**
 * Base class for transaction editor contexts.
 */
export interface TransactionEditorContextBase {
  isFormDataValid(formData: TransactionEditorFormFields): boolean
  areChangesPresent(formData: TransactionEditorFormFields): boolean
  initializeFormFields(
    queryParams: Record<string, string | null | (string | null)[]>,
  ): TransactionEditorFormFields
  getActions(formData: TransactionEditorFormFields): TransactionEditorAction[]
}

/**
 * Editor context that's used when the user starts editing a new transaction,
 * not related to any saved transaction or draft.
 */
export class NewTransactionEditorContext implements TransactionEditorContextBase {
  isFormDataValid(formData: TransactionEditorFormFields): boolean {
    return (
      isFormDataValidForDraftSave(formData) || isFormDataValidForTransactionSubmission(formData)
    )
  }

  areChangesPresent(formData: TransactionEditorFormFields): boolean {
    return (
      formData.timestamp !== null ||
      formData.amount !== null ||
      formData.templateName !== null ||
      formData.currency !== null ||
      formData.description !== null ||
      formData.internalTransfer !== null ||
      formData.vendor !== null ||
      formData.creditedAccountId !== null ||
      formData.debitedAccountId !== null ||
      formData.lineItems.length > 0 ||
      formData.tags.length > 0 ||
      formData.attachmentsToUpload.length > 0
    )
  }

  initializeFormFields(
    queryParams: Record<string, string | null | (string | null)[]>,
  ): TransactionEditorFormFields {
    const fields = defaultEmptyFormFields()
    fields.timestamp = getDatetimeLocalValueForNow()
    if ('credited-account' in queryParams) {
      fields.creditedAccountId = parseInt(queryParams['credited-account'] as string)
    }
    if ('debited-account' in queryParams) {
      fields.debitedAccountId = parseInt(queryParams['debited-account'] as string)
    }
    return fields
  }

  getActions(formData: TransactionEditorFormFields): TransactionEditorAction[] {
    return [
      {
        name: 'Save',
        disabled: !(
          this.areChangesPresent(formData) && isFormDataValidForTransactionSubmission(formData)
        ),
        callback: async (formData, route, router) => {
          const api = new TransactionApiClient(getSelectedProfile(route))
          // Assume that form data is valid!
          const data = toTransactionPayload(formData)
          const txn = await api.addTransaction(data, formData.attachmentsToUpload)
          await router.replace(`/profiles/${getSelectedProfile(route)}/transactions/${txn.id}`)
        },
      },
      {
        name: 'Save Draft',
        disabled: !this.areChangesPresent(formData) || !isFormDataValidForDraftSave(formData),
        callback: async (formData, route, router) => {
          const api = new TransactionApiClient(getSelectedProfile(route))
          const data = toDraftPayload(formData)
          const draft = await api.addDraft(data, formData.attachmentsToUpload)
          await router.replace(
            `/profiles/${getSelectedProfile(route)}/transaction-drafts/${draft.id}`,
          )
        },
      },
      {
        name: 'Cancel',
        disabled: false,
        callback: async (_formData, route, router) => {
          await goBackOrHome(router, route)
        },
      },
    ]
  }
}

/**
 * Editor context for when the user is editing a transaction.
 */
export class TransactionEditorContext implements TransactionEditorContextBase {
  private existingTransaction: TransactionDetail

  constructor(existingTransaction: TransactionDetail) {
    this.existingTransaction = existingTransaction
  }

  isFormDataValid(formData: TransactionEditorFormFields): boolean {
    return isFormDataValidForTransactionSubmission(formData)
  }

  areChangesPresent(formData: TransactionEditorFormFields): boolean {
    const tx: TransactionDetail = this.existingTransaction
    const tagsChanged =
      formData.tags.every((t) => tx.tags.includes(t)) &&
      tx.tags.every((t) => formData.tags.includes(t))
    const lineItemsChanged =
      JSON.stringify(formData.lineItems) !== JSON.stringify(formData.lineItems)
    const attachmentsChanged =
      formData.attachmentsToUpload.length > 0 || formData.removedAttachmentIds.length > 0

    const timestampChanged = new Date(formData.timestamp ?? 0).toISOString() !== tx.timestamp
    const amountChanged =
      floatMoneyToInteger(formData.amount ?? 0, formData.currency ?? tx.currency) !== tx.amount
    const currencyChanged = formData.currency?.code !== tx.currency.code
    const descriptionChanged = formData.description !== tx.description
    const internalTransferChanged = formData.internalTransfer !== tx.internalTransfer
    const vendorChanged = formData.vendor?.id !== tx.vendor?.id
    const categoryChanged = formData.categoryId !== (tx.category?.id ?? null)
    const creditedAccountChanged = formData.creditedAccountId !== (tx.creditedAccount?.id ?? null)
    const debitedAccountChanged = formData.debitedAccountId !== (tx.debitedAccount?.id ?? null)

    return (
      tagsChanged ||
      lineItemsChanged ||
      attachmentsChanged ||
      timestampChanged ||
      amountChanged ||
      currencyChanged ||
      descriptionChanged ||
      internalTransferChanged ||
      vendorChanged ||
      categoryChanged ||
      creditedAccountChanged ||
      debitedAccountChanged
    )
  }

  initializeFormFields(): TransactionEditorFormFields {
    const tx = this.existingTransaction
    return {
      timestamp: getLocalDateTimeStringFromUTCTimestamp(tx.timestamp),
      amount: integerMoneyToFloat(tx.amount, tx.currency),
      templateName: null,
      currency: tx.currency,
      description: tx.description,
      internalTransfer: tx.internalTransfer,
      vendor: tx.vendor ?? null,
      categoryId: tx.category?.id ?? null,
      creditedAccountId: tx.creditedAccount?.id ?? null,
      debitedAccountId: tx.debitedAccount?.id ?? null,
      lineItems: [...tx.lineItems],
      tags: [...tx.tags],
      attachmentsToUpload: [],
      removedAttachmentIds: [],
      existingAttachments: [...tx.attachments],
    }
  }

  getActions(formData: TransactionEditorFormFields): TransactionEditorAction[] {
    return [
      {
        name: 'Save',
        disabled: !this.areChangesPresent(formData) || !this.isFormDataValid(formData),
        callback: async (formData, route, router) => {
          const api = new TransactionApiClient(getSelectedProfile(route))
          // Assume that form data is valid!
          const data = toTransactionPayload(formData)
          const txn = await api.updateTransaction(
            this.existingTransaction.id,
            data,
            formData.attachmentsToUpload,
          )
          await router.replace(`/profiles/${getSelectedProfile(route)}/transactions/${txn.id}`)
        },
      },
      {
        name: 'Cancel',
        disabled: false,
        callback: async (_formData, route, router) => {
          await goBackOrHome(router, route)
        },
      },
    ]
  }
}

/**
 * Editor context for when the user is editing an existing draft.
 */
export class DraftEditorContext implements TransactionEditorContextBase {
  private existingDraft: TransactionDraftResponse

  constructor(existingDraft: TransactionDraftResponse) {
    this.existingDraft = existingDraft
  }

  isFormDataValid(formData: TransactionEditorFormFields): boolean {
    const result =
      (formData.amount === null || formData.currency !== null) &&
      (formData.amount === null || formData.amount > 0) &&
      (formData.creditedAccountId === null ||
        formData.debitedAccountId === null ||
        formData.creditedAccountId !== formData.debitedAccountId) &&
      (formData.templateName === null || formData.templateName.length <= 32)
    console.log('Checking draft editor valid:', formData, result)
    return result
  }

  areChangesPresent(): boolean {
    return true
  }

  initializeFormFields(): TransactionEditorFormFields {
    const d = this.existingDraft
    const fields = defaultEmptyFormFields()
    if (d.templateName !== null && d.templateName.length > 0) {
      fields.templateName = d.templateName
    }
    if (d.timestamp !== null) {
      fields.timestamp = getLocalDateTimeStringFromUTCTimestamp(d.timestamp)
    }
    if (d.amount !== null && d.currency !== null) {
      fields.currency = d.currency
      fields.amount = integerMoneyToFloat(d.amount, d.currency)
    }
    fields.description = d.description
    fields.internalTransfer = d.internalTransfer
    if (d.vendor !== null) {
      fields.vendor = {
        id: d.vendor.id,
        name: d.vendor.name,
        description: '',
      }
      // TODO: Update TransactionDraftResponse format to include full vendor data.
    }
    if (d.category !== null) {
      fields.categoryId = d.category.id
    }
    if (d.creditedAccount !== null) {
      fields.creditedAccountId = d.creditedAccount.id
    }
    if (d.debitedAccount !== null) {
      fields.debitedAccountId = d.debitedAccount.id
    }
    fields.lineItems = [...d.lineItems]
    fields.tags = [...d.tags]
    fields.existingAttachments = [...d.attachments]
    return fields
  }

  getActions(formData: TransactionEditorFormFields): TransactionEditorAction[] {
    return [
      {
        name: 'Save Draft',
        disabled: !this.areChangesPresent() || !this.isFormDataValid(formData),
        callback: async (formData, route, router) => {
          const api = new TransactionApiClient(getSelectedProfile(route))
          const data = toDraftPayload(formData)
          const draft = await api.updateDraft(
            this.existingDraft.id,
            data,
            formData.attachmentsToUpload,
          )
          await router.replace(
            `/profiles/${getSelectedProfile(route)}/transaction-drafts/${draft.id}`,
          )
        },
      },
      {
        name: 'Submit Transaction',
        disabled: !this.areChangesPresent() || !isFormDataValidForTransactionSubmission(formData),
        callback: async (formData, route, router) => {
          const api = new TransactionApiClient(getSelectedProfile(route))
          // First call the normal "Save" callback from the NewTransactionEditorContext.
          const tec = new NewTransactionEditorContext()
          const saveTxnAction = tec.getActions(formData).find((a) => a.name === 'Save')!
          await saveTxnAction.callback(formData, route, router)
          // Then if this is not a template draft, delete it.
          if (formData.templateName === null || formData.templateName.length === 0) {
            await api.deleteDraft(this.existingDraft.id)
          }
        },
      },
      {
        name: 'Cancel',
        disabled: false,
        callback: async (_formData, route, router) => {
          await goBackOrHome(router, route)
        },
      },
    ]
  }
}

// Helper functions below here!

/**
 * Obtains an editor context by determining what the user is doing based on the
 * route they've navigated to.
 * @param route The current route (which tells us what intent the user has).
 * @returns A promise that resolves to an editor context.
 */
export async function loadEditorContextFromRoute(
  route: RouteLocation,
): Promise<TransactionEditorContextBase> {
  const transactionApi = new TransactionApiClient(getSelectedProfile(route))
  if (route.name === 'edit-transaction') {
    const transactionIdStr = route.params.id
    if (transactionIdStr && typeof transactionIdStr === 'string') {
      const transactionId = parseInt(transactionIdStr)
      const existingTransaction = await transactionApi.getTransaction(transactionId)
      return new TransactionEditorContext(existingTransaction)
    }
  } else if (route.name === 'edit-draft') {
    const draftIdStr = route.params.id
    if (draftIdStr && typeof draftIdStr === 'string') {
      const draftId = parseInt(draftIdStr)
      const existingDraft = await transactionApi.getDraft(draftId)
      return new DraftEditorContext(existingDraft)
    }
  }

  return new NewTransactionEditorContext()
}

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)
}

function toDraftPayload(formData: TransactionEditorFormFields): TransactionDraftPayload {
  if (typeof formData.amount === 'string') {
    formData.amount = null
  }
  if (formData.amount !== null && formData.currency !== null) {
    formData.amount = floatMoneyToInteger(formData.amount, formData.currency)
  } else {
    formData.amount = null
    formData.currency = null
  }
  let isoTimestamp = null
  if (formData.timestamp !== null && formData.timestamp.length > 0) {
    isoTimestamp = new Date(formData.timestamp).toISOString()
  }
  return {
    templateName: formData.templateName,
    timestamp: isoTimestamp,
    amount: formData.amount,
    currencyCode: formData.currency?.code ?? null,
    description: formData.description,
    internalTransfer: formData.internalTransfer,
    vendorId: formData.vendor?.id ?? null,
    categoryId: formData.categoryId,
    creditedAccountId: formData.creditedAccountId,
    debitedAccountId: formData.debitedAccountId,
    tags: [...formData.tags],
    lineItems: [...formData.lineItems].map((li) => {
      return {
        valuePerItem: li.valuePerItem,
        quantity: li.quantity,
        description: li.description,
        categoryId: li.category?.id ?? null,
      }
    }),
    attachmentIdsToRemove: [...formData.removedAttachmentIds],
  }
}

function toTransactionPayload(formData: TransactionEditorFormFields): AddTransactionPayload {
  const payload: AddTransactionPayload = {
    timestamp: new Date(formData.timestamp!).toISOString(),
    amount: floatMoneyToInteger(formData.amount!, formData.currency!),
    currencyCode: formData.currency!.code,
    description: formData.description ?? '',
    internalTransfer: formData.internalTransfer ?? false,
    vendorId: formData.vendor?.id ?? null,
    categoryId: formData.categoryId,
    creditedAccountId: formData.creditedAccountId,
    debitedAccountId: formData.debitedAccountId,
    tags: formData.tags,
    lineItems: formData.lineItems.map((li) => {
      return { ...li, categoryId: li.category?.id ?? null }
    }),
    attachmentIdsToRemove: formData.removedAttachmentIds,
  }
  return payload
}

async function goBackOrHome(router: Router, route: RouteLocation) {
  if (window.history.length > 0) {
    await router.back()
  } else {
    await router.replace(`/profiles/${getSelectedProfile(route)}`)
  }
}

function isFormDataValidForTransactionSubmission(formData: TransactionEditorFormFields): boolean {
  return (
    formData.timestamp !== null &&
    formData.timestamp.length > 0 &&
    formData.amount !== null &&
    formData.amount > 0 &&
    formData.currency !== null &&
    formData.description !== null &&
    formData.description.length > 0 &&
    (formData.creditedAccountId !== null || formData.debitedAccountId !== null) &&
    formData.creditedAccountId !== formData.debitedAccountId
  )
}

function isFormDataValidForDraftSave(formData: TransactionEditorFormFields): boolean {
  return (
    formData.amount !== null &&
    formData.amount > 0 &&
    formData.timestamp !== null &&
    formData.currency !== null
  )
}
