#!/usr/bin/env ruby

require "csv"
require "date"
require "pathname"

BUCKET_ORDER = [
  "Mom care",
  "Lodging",
  "Travel / transport",
  "Therapy / medical",
  "Legal",
  "Food / basic household",
  "Software / subscriptions",
  "Equipment / device costs",
  "Juno expense",
  "Personal / political",
  "Adjustments / non-spending activity",
  "Unclassified",
].freeze

SOFTWARE_MERCHANTS = [
  "Openai *chatgpt Subscr",
  "Openai",
  "elevenlabs.io",
  "Meshy",
  "Dreamhost",
  "Audible",
  "Kindroid Ai",
  "Epoch.Com   *morcansdi",
  "Ccbill.Com *xchar.Ai",
  "Subscribestar",
].freeze

FOOD_MERCHANTS = [
  "Hudson St2401",
  "Sat Adinas Market",
  "Walgreens",
].freeze

TRAVEL_MERCHANTS = [
  "Southwest Airlines",
  "ALAMO RENT-A-CAR",
  "Lyft",
  "7-Eleven",
  "Carts Sfo Smar00 Of 00",
  "Nyx*smartecarte Cart/s",
  "Expedia",
  "Expedia 733880746920771111 Expedia Group Way W Seattle 98119 Wa Usa (return)",
].freeze

def usage!
  warn "Usage: apple_card_pdf_report.rb INPUT.csv OUTPUT.tex"
  exit 1
end

def format_amount(amount)
  format("%.2f", amount)
end

def format_short_date(date)
  date.strftime("%m-%d")
end

def tex_escape(text)
  text.to_s
      .gsub("\\", "\\textbackslash{}")
      .gsub("{", "\\{")
      .gsub("}", "\\}")
      .gsub("$", "\\$")
      .gsub("&", "\\&")
      .gsub("%", "\\%")
      .gsub("#", "\\#")
      .gsub("_", "\\_")
      .gsub("^", "\\^{}")
      .gsub("~", "\\~{}")
end

def classify_bucket(row)
  return "Payments (excluded from spending)" if row[:type] == "Payment"
  return "Adjustments / non-spending activity" if row[:type] == "Debit" && row[:merchant] == "Daily Cash Adjustment"
  return "Juno expense" if row[:type] == "Installment" && row[:merchant] == "Monthly Installments (9 Of 12)"
  return "Mom care" if ["Vtg*discovery Village", "Amazon Marketplace"].include?(row[:merchant])
  return "Lodging" if ["BEST WESTERN HOTELS", "HOLIDAY INNS", "Best Western Premier C524 Sutter St San Francisco94102 Ca Usa (return)"].include?(row[:merchant])
  return "Travel / transport" if TRAVEL_MERCHANTS.include?(row[:merchant])
  return "Therapy / medical" if ["St Louis Counseling Ce", "Gifthealth*lillydirect"].include?(row[:merchant])
  return "Legal" if row[:merchant] == "Jones Family Law..."
  return "Food / basic household" if row[:category] == "Restaurants" || row[:category] == "Grocery" || FOOD_MERCHANTS.include?(row[:merchant])
  return "Software / subscriptions" if SOFTWARE_MERCHANTS.include?(row[:merchant])
  return "Equipment / device costs" if row[:merchant] == "Apple Union Square"
  return "Personal / political" if row[:merchant] == "ActBlue"

  "Unclassified"
end

def display_label(row)
  case row[:merchant]
  when "Vtg*discovery Village"
    "Discovery Village"
  when "Amazon Marketplace"
    "Amazon medical supplies for Mom"
  when "BEST WESTERN HOTELS"
    "Best Western"
  when "HOLIDAY INNS"
    "Holiday Inn"
  when "Best Western Premier C524 Sutter St San Francisco94102 Ca Usa (return)"
    "Best Western credit"
  when "Southwest Airlines"
    "Southwest"
  when "ALAMO RENT-A-CAR"
    "Alamo rental car"
  when "7-Eleven"
    "7-Eleven gas"
  when "Carts Sfo Smar00 Of 00"
    "SFO cart"
  when "Nyx*smartecarte Cart/s"
    "SmarteCarte"
  when "Expedia"
    "Expedia"
  when "Expedia 733880746920771111 Expedia Group Way W Seattle 98119 Wa Usa (return)"
    "Expedia credit"
  when "St Louis Counseling Ce"
    "St. Louis Counseling Center"
  when "Gifthealth*lillydirect"
    "LillyDirect / Gifthealth"
  when "Jones Family Law..."
    "Jones Family Law"
  when "Hudson St2401"
    "Hudson News / grocery"
  when "Sat Adinas Market"
    "Adina's Market"
  when "Openai *chatgpt Subscr"
    "ChatGPT subscription"
  when "Openai"
    "OpenAI usage"
  when "elevenlabs.io"
    "ElevenLabs"
  when "Apple Union Square"
    "Apple Union Square laptop screen repair"
  when "Monthly Installments (9 Of 12)"
    "Monthly installment for Juno's laptop"
  when "ActBlue"
    "ActBlue donation to AOC"
  when "Daily Cash Adjustment"
    "Daily Cash adjustment"
  else
    row[:merchant]
  end
end

input_path = ARGV[0]
output_path = ARGV[1]
usage! unless input_path && output_path

input = Pathname.new(input_path)
output = Pathname.new(output_path)

rows = CSV.read(input, headers: true).map do |row|
  parsed = {
    transaction_date: Date.strptime(row.fetch("Transaction Date"), "%m/%d/%Y"),
    clearing_date: Date.strptime(row.fetch("Clearing Date"), "%m/%d/%Y"),
    description: row.fetch("Description"),
    merchant: row.fetch("Merchant"),
    category: row.fetch("Category"),
    type: row.fetch("Type"),
    amount: row.fetch("Amount (USD)").to_f,
    purchased_by: row.fetch("Purchased By"),
  }
  parsed[:bucket] = classify_bucket(parsed)
  parsed[:display_label] = display_label(parsed)
  parsed
end

dates = rows.map { |row| row[:transaction_date] }
type_totals = Hash.new(0.0)
bucket_totals = Hash.new(0.0)

rows.each do |row|
  type_totals[row[:type]] += row[:amount]
  bucket_totals[row[:bucket]] += row[:amount]
end

cost_rows = rows.reject { |row| row[:type] == "Payment" }
cost_dates = cost_rows.map { |row| row[:transaction_date] }
cost_type_totals = Hash.new(0.0)
cost_bucket_totals = Hash.new(0.0)

cost_rows.each do |row|
  cost_type_totals[row[:type]] += row[:amount]
  cost_bucket_totals[row[:bucket]] += row[:amount]
end

sorted_rows = cost_rows.sort_by { |row| [row[:transaction_date], row[:clearing_date], row[:merchant], row[:amount]] }.reverse
bucket_rows = cost_rows.group_by { |row| row[:bucket] }
cost_total = cost_type_totals.values.sum

tex = +"\\documentclass[11pt]{article}\n"
tex << "\\usepackage[margin=0.7in]{geometry}\n"
tex << "\\usepackage{array}\n"
tex << "\\usepackage{booktabs}\n"
tex << "\\usepackage{longtable}\n"
tex << "\\usepackage{pdflscape}\n"
tex << "\\usepackage[hidelinks]{hyperref}\n"
tex << "\\setlength{\\parindent}{0pt}\n"
tex << "\\setlength{\\parskip}{0.6em}\n"
tex << "\\begin{document}\n"
tex << "{\\LARGE Apple Card Cost Report}\\\\\n"
tex << "Generated #{Date.today}\\\\\n"
tex << "Source file: \\texttt{#{tex_escape(input.basename.to_s)}}\\\\\n"
tex << "Imported from: \\texttt{#{tex_escape(input.to_s)}}\n\n"

tex << "\\section*{Summary}\n"
tex << "\\begin{itemize}\n"
tex << "\\item Rows: #{cost_rows.size}\n"
tex << "\\item Actual transaction date range: #{cost_dates.min} to #{cost_dates.max}\n"
tex << "\\item Purchases: #{format_amount(cost_type_totals["Purchase"])}\n"
tex << "\\item Credits: #{format_amount(cost_type_totals["Credit"])}\n"
tex << "\\item Debit / installment adjustments: #{format_amount(cost_type_totals["Debit"] + cost_type_totals["Installment"])}\n"
tex << "\\item Net cost activity: #{format_amount(cost_total)}\n"
tex << "\\end{itemize}\n"

tex << "\\section*{Bucket Totals}\n"
tex << "\\begin{tabular}{p{3.2in}r}\n"
tex << "\\toprule\n"
tex << "Bucket & Total (USD)\\\\\n"
tex << "\\midrule\n"
BUCKET_ORDER.each do |bucket|
  next unless cost_bucket_totals.key?(bucket)

  tex << "#{tex_escape(bucket)} & #{format_amount(cost_bucket_totals[bucket])}\\\\\n"
end
tex << "\\bottomrule\n"
tex << "\\end{tabular}\n"

tex << "\\begin{landscape}\n"
tex << "\\section*{Full Transaction Ledger}\n"
tex << "\\small\n"
tex << "\\begin{longtable}{>{\\raggedright\\arraybackslash}p{1.5cm} >{\\raggedright\\arraybackslash}p{1.5cm} >{\\raggedright\\arraybackslash}p{1.2cm} >{\\raggedright\\arraybackslash}p{1.8cm} >{\\raggedright\\arraybackslash}p{2.5cm} r >{\\raggedright\\arraybackslash}p{3.2cm} >{\\raggedright\\arraybackslash}p{7.2cm}}\n"
tex << "\\toprule\n"
tex << "Txn Date & Clear Date & Type & Apple Cat & Bucket & Amount & Merchant & Description\\\\\n"
tex << "\\midrule\n"
tex << "\\endfirsthead\n"
tex << "\\toprule\n"
tex << "Txn Date & Clear Date & Type & Apple Cat & Bucket & Amount & Merchant & Description\\\\\n"
tex << "\\midrule\n"
tex << "\\endhead\n"
tex << "\\bottomrule\n"
tex << "\\endfoot\n"
sorted_rows.each do |row|
  tex << [
    format_short_date(row[:transaction_date]),
    format_short_date(row[:clearing_date]),
    tex_escape(row[:type]),
    tex_escape(row[:category]),
    tex_escape(row[:bucket]),
    format_amount(row[:amount]),
    tex_escape(row[:merchant]),
    tex_escape(row[:description]),
  ].join(" & ")
  tex << "\\\\\n"
end
tex << "\\end{longtable}\n"
tex << "\\normalsize\n"
tex << "\\end{landscape}\n"
tex << "\\end{document}\n"

output.parent.mkpath
output.write(tex)
