import { isIsoDate } from "@/lib/calendar"

/**
 * Reading the year's holiday calendar.
 *
 * Pure, with nothing under it but the calendar's own date check, so the rules
 * can be tested without a database and so the screen that pastes a file in can
 * take them.
 *
 * The calendar arrives once a year as a file somebody exports from a
 * spreadsheet, which is why this is forgiving about the shape and unforgiving
 * about the content. A stray blank line or a byte order mark is not worth
 * refusing a year's work over. A date that is not a date is: every one of these
 * moves real due dates, and a duty on a day the factory is shut is a duty
 * nobody does.
 */

export type Holiday = { date: string; name: string }

export type ParseResult =
  | { ok: true; holidays: Holiday[] }
  | { ok: false; line: number; problem: string }

/**
 * Split one CSV line into its fields, honouring quotes.
 *
 * Written because the first version of this file said "everything after the
 * first comma is the name", and the client's own export has four columns and a
 * quoted description with a comma inside it. That shortcut would have imported
 * "Makha Bucha,Theravada (Thailand),Commemorates..." as the name of a day.
 *
 * `""` inside a quoted field is one quote, which is what every spreadsheet
 * writes and the only escape this needs to know.
 */
function fields(line: string): string[] {
  const out: string[] = []
  let value = ""
  let quoted = false

  for (let i = 0; i < line.length; i++) {
    const c = line[i]
    if (quoted) {
      if (c === '"') {
        if (line[i + 1] === '"') {
          value += '"'
          i++
        } else {
          quoted = false
        }
      } else {
        value += c
      }
    } else if (c === '"') {
      quoted = true
    } else if (c === ",") {
      out.push(value.trim())
      value = ""
    } else {
      value += c
    }
  }
  out.push(value.trim())
  return out
}

/**
 * The first column is the date and the second is the name. Anything after them
 * is ignored rather than refused: the calendar comes out of a spreadsheet that
 * may carry a tradition, a description or a note, and none of that is something
 * this table has an opinion about.
 *
 * A header is optional, recognised by its first field not being a date.
 */
export function parseHolidays(text: string): ParseResult {
  const lines = text
    .replace(/^﻿/, "")
    .split(/\r?\n/)
    .map((line, i) => ({ line: i + 1, text: line.trim() }))
    .filter((l) => l.text !== "")

  if (lines.length === 0) {
    return { ok: false, line: 1, problem: "There is nothing in this file." }
  }

  // A header is optional and recognised by its first field not being a date.
  const rows = isIsoDate(fields(lines[0]!.text)[0] ?? "")
    ? lines
    : lines.slice(1)

  const holidays: Holiday[] = []
  const seen = new Map<string, number>()

  for (const row of rows) {
    const cells = fields(row.text)
    if (cells.length < 2) {
      return {
        ok: false,
        line: row.line,
        problem: "This line has one field, so it has a date and no name.",
      }
    }

    const date = cells[0]!
    const name = cells[1]!

    if (!isIsoDate(date)) {
      return {
        ok: false,
        line: row.line,
        problem: `"${date}" is not a date. Dates are written 2026-12-31.`,
      }
    }
    if (!name) {
      return {
        ok: false,
        line: row.line,
        problem:
          "This day has no name. Somebody has to know why the factory is shut.",
      }
    }
    /**
     * Two rows for one day is a mistake worth stopping for rather than
     * resolving. The table's primary key is the date, so one of them would win
     * silently, and which one would depend on the order of a file.
     */
    const already = seen.get(date)
    if (already) {
      return {
        ok: false,
        line: row.line,
        problem: `${date} is already on line ${already}.`,
      }
    }

    seen.set(date, row.line)
    holidays.push({ date, name })
  }

  if (holidays.length === 0) {
    return { ok: false, line: 1, problem: "There are no days in this file." }
  }

  return { ok: true, holidays }
}

/** The years a file covers, so a screen can say what it is about to replace. */
export function yearsIn(holidays: Holiday[]): number[] {
  return [...new Set(holidays.map((h) => Number(h.date.slice(0, 4))))].sort()
}
