class PlaidAccount < ApplicationRecord
  include Plaidable

  TYPE_MAPPING = {
    "depository" => Depository,
    "credit" => CreditCard,
    "loan" => Loan,
    "investment" => Investment,
    "other" => OtherAsset
  }

  belongs_to :plaid_item

  has_one :account, dependent: :destroy

  accepts_nested_attributes_for :account

  class << self
    def find_or_create_from_plaid_data!(plaid_data, family)
      find_or_create_by!(plaid_id: plaid_data.account_id) do |a|
        a.account = family.accounts.new(
          name: plaid_data.name,
          balance: plaid_data.balances.current,
          currency: plaid_data.balances.iso_currency_code,
          accountable: TYPE_MAPPING[plaid_data.type].new
        )
      end
    end
  end

  def sync_account_data!(plaid_account_data)
    update!(
      current_balance: plaid_account_data.balances.current,
      available_balance: plaid_account_data.balances.available,
      currency: plaid_account_data.balances.iso_currency_code,
      plaid_type: plaid_account_data.type,
      plaid_subtype: plaid_account_data.subtype,
      account_attributes: {
        id: account.id,
        balance: plaid_account_data.balances.current
      }
    )
  end

  def sync_investments!(transactions:, holdings:, securities:)
    transactions.each do |transaction|
      if transaction.type == "cash"
        new_transaction = account.entries.find_or_create_by!(plaid_id: transaction.investment_transaction_id) do |t|
          t.name = transaction.name
          t.amount = transaction.amount
          t.currency = transaction.iso_currency_code
          t.date = transaction.date
          t.marked_as_transfer = transaction.subtype.in?(%w[deposit withdrawal])
          t.entryable = Account::Transaction.new
        end
      else
        security = get_security(transaction.security, securities)
        next if security.nil?
        new_transaction = account.entries.find_or_create_by!(plaid_id: transaction.investment_transaction_id) do |t|
          t.name = transaction.name
          t.amount = transaction.quantity * transaction.price
          t.currency = transaction.iso_currency_code
          t.date = transaction.date
          t.entryable = Account::Trade.new(
            security: security,
            qty: transaction.quantity,
            price: transaction.price,
            currency: transaction.iso_currency_code
          )
        end
      end
    end

    # Update only the current day holdings.  The account sync will populate historical values based on trades.
    holdings.each do |holding|
      internal_security = get_security(holding.security, securities)
      next if internal_security.nil?

      existing_holding = account.holdings.find_or_initialize_by(
        security: internal_security,
        date: Date.current,
        currency: holding.iso_currency_code
      )

      existing_holding.qty = holding.quantity
      existing_holding.price = holding.institution_price
      existing_holding.amount = holding.quantity * holding.institution_price
      existing_holding.save!
    end
  end

  def sync_credit_data!(plaid_credit_data)
    account.update!(
      accountable_attributes: {
        id: account.accountable_id,
        minimum_payment: plaid_credit_data.minimum_payment_amount,
        apr: plaid_credit_data.aprs.first&.apr_percentage
      }
    )
  end

  def sync_mortgage_data!(plaid_mortgage_data)
    create_initial_loan_balance(plaid_mortgage_data)

    account.update!(
      accountable_attributes: {
        id: account.accountable_id,
        rate_type: plaid_mortgage_data.interest_rate&.type,
        interest_rate: plaid_mortgage_data.interest_rate&.percentage
      }
    )
  end

  def sync_student_loan_data!(plaid_student_loan_data)
    create_initial_loan_balance(plaid_student_loan_data)

    account.update!(
      accountable_attributes: {
        id: account.accountable_id,
        rate_type: "fixed",
        interest_rate: plaid_student_loan_data.interest_rate_percentage
      }
    )
  end

  def sync_transactions!(added:, modified:, removed:)
    added.each do |plaid_txn|
      account.entries.find_or_create_by!(plaid_id: plaid_txn.transaction_id) do |t|
        t.name = plaid_txn.name
        t.amount = plaid_txn.amount
        t.currency = plaid_txn.iso_currency_code
        t.date = plaid_txn.date
        t.marked_as_transfer = transfer?(plaid_txn)
        t.entryable = Account::Transaction.new(
          category: get_category(plaid_txn.personal_finance_category.primary),
          merchant: get_merchant(plaid_txn.merchant_name)
        )
      end
    end

    modified.each do |plaid_txn|
      existing_txn = account.entries.find_by(plaid_id: plaid_txn.transaction_id)

      existing_txn.update!(
        amount: plaid_txn.amount,
        date: plaid_txn.date
      )
    end

    removed.each do |plaid_txn|
      account.entries.find_by(plaid_id: plaid_txn.transaction_id)&.destroy
    end
  end

  private
    def family
      plaid_item.family
    end

    def get_security(plaid_security, securities)
      security = nil

      if plaid_security.ticker_symbol.present?
        security = plaid_security
      else
        security = securities.find { |s| s.security_id == plaid_security.proxy_security_id }
      end

      Security.find_or_create_by!(
        ticker: security.ticker_symbol,
        exchange_mic: security.market_identifier_code || "XNAS",
        country_code: "US"
      ) if security.present?
    end

    def transfer?(plaid_txn)
      transfer_categories = [ "TRANSFER_IN", "TRANSFER_OUT", "LOAN_PAYMENTS" ]

      transfer_categories.include?(plaid_txn.personal_finance_category.primary)
    end

    def create_initial_loan_balance(loan_data)
      if loan_data.origination_principal_amount.present? && loan_data.origination_date.present?
        account.entries.find_or_create_by!(plaid_id: loan_data.account_id) do |e|
          e.name = "Initial Principal"
          e.amount = loan_data.origination_principal_amount
          e.currency = account.currency
          e.date = loan_data.origination_date
          e.entryable = Account::Valuation.new
        end
      end
    end

    # See https://plaid.com/documents/transactions-personal-finance-category-taxonomy.csv
    def get_category(plaid_category)
      ignored_categories = [ "BANK_FEES", "TRANSFER_IN", "TRANSFER_OUT", "LOAN_PAYMENTS", "OTHER" ]

      return nil if ignored_categories.include?(plaid_category)

      family.categories.find_or_create_by!(name: plaid_category.titleize)
    end

    def get_merchant(plaid_merchant_name)
      return nil if plaid_merchant_name.blank?

      family.merchants.find_or_create_by!(name: plaid_merchant_name)
    end
end
