/**
 * Clockify CSV parsing helpers.
 *
 * Clockify's CSV column layout:
 *   Project, Client, Description, Task, User, Group, Email, Tags,
 *   Billable, Start Date, Start Time, End Date, End Time,
 *   Duration (h), Duration (decimal), Billable Rate (USD), Billable Amount (USD)
 *
 * Dates and times are LOCAL, without TZ. We keep them naive end-to-end.
 */

import Papa from 'papaparse'

export type DateFormat = 'YYYY-MM-DD' | 'MM/DD/YYYY' | 'DD/MM/YYYY'
export type TimeFormat = '24h' | '12h'

export type ParsedEntry = {
  source_user_name: string
  source_user_email: string
  operator: string         // raw Clockify "Client" column = operator agency
  client: string | null    // parsed end-client (left of " : ")
  project: string          // remainder of CSV "Project" field
  description: string
  billable: boolean
  start_at: string   // 'YYYY-MM-DD HH:MM:SS' (naive)
  end_at: string | null
  duration_seconds: number | null
  source_rate_usd: number | null
  source_amount_usd: number | null
}

/**
 * Splits the legacy "{Client} : {Project}" convention into separate fields.
 * The delimiter is literally " : " (space-colon-space) — matches how the
 * data has been written for years. If there's no " : ", client is null
 * and the whole string is treated as the project name.
 */
export function splitClientProject(raw: string): {
  client: string | null
  project: string
} {
  const sep = ' : '
  const idx = raw.indexOf(sep)
  if (idx === -1) return { client: null, project: raw.trim() }
  return {
    client: raw.substring(0, idx).trim() || null,
    project: raw.substring(idx + sep.length).trim(),
  }
}

export type ParseResult = {
  entries: ParsedEntry[]
  errors: { row: number; message: string }[]
  totalRows: number
  uniqueEmails: string[]
  dateRange: { min: string; max: string } | null
}

// Accepts both Clockify and Toggl CSV column layouts. Clockify uses
// "User" / "End date" / "Billable". Toggl uses "Member" / "Stop date"
// and may omit Billable from the Detailed Report.
const HEADERS = {
  project: ['project'],
  client: ['client'],
  description: ['description'],
  task: ['task'],
  user: ['user', 'member'],
  email: ['email'],
  billable: ['billable'],
  startDate: ['start date', 'startdate'],
  startTime: ['start time', 'starttime'],
  endDate: ['end date', 'enddate', 'stop date', 'stopdate'],
  endTime: ['end time', 'endtime', 'stop time', 'stoptime'],
  duration: ['duration (h)', 'duration'],
  rate: ['billable rate (usd)', 'billable rate'],
  amount: ['billable amount (usd)', 'billable amount'],
} as const

function normHeader(s: string): string {
  return s.toLowerCase().trim()
}

function buildHeaderIndex(headers: string[]): Record<keyof typeof HEADERS, number> {
  const index: Record<string, number> = {}
  for (const [key, candidates] of Object.entries(HEADERS)) {
    let found = -1
    for (let i = 0; i < headers.length; i++) {
      if (candidates.includes(normHeader(headers[i]) as never)) {
        found = i
        break
      }
    }
    index[key] = found
  }
  return index as Record<keyof typeof HEADERS, number>
}

/**
 * Sniff the date format used across a list of date strings. Returns
 * `'YYYY-MM-DD'` if any input has a leading 4-digit year;
 * `'DD/MM/YYYY'` if any input's first slot exceeds 12 (so it can't
 * be a month); `'MM/DD/YYYY'` if any input's second slot exceeds 12
 * (so it can't be a day-of-month interpreted as month);
 * `null` when every input is ambiguous (every slot ≤ 12).
 *
 * Mixed inputs (some DD/MM, some MM/DD in the same CSV) are a data
 * quality bug we don't try to handle — the parser picks whichever
 * unambiguous signal it sees first.
 */
export function detectDateFormat(dates: string[]): DateFormat | null {
  let sawIso = false
  let sawDDMM = false
  let sawMMDD = false
  for (const raw of dates) {
    const s = raw.trim()
    if (!s) continue
    if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(s)) {
      sawIso = true
      continue
    }
    const m = s.match(/^(\d{1,4})[/\-.](\d{1,4})[/\-.](\d{1,4})$/)
    if (!m) continue
    const a = Number(m[1])
    const b = Number(m[2])
    // a > 12 → first slot can't be a month → must be DD/MM.
    if (a > 12) sawDDMM = true
    // b > 12 → second slot can't be a day-interpreted-as-month → must be MM/DD.
    else if (b > 12) sawMMDD = true
  }
  // Priority: ISO is unambiguous when present. After that, prefer
  // whichever non-ambiguous signal fired. (We don't expect both to
  // fire in a sane CSV; if they do, return null and let the caller
  // fall back to the form selector — there's no right answer.)
  if (sawIso && !sawDDMM && !sawMMDD) return 'YYYY-MM-DD'
  if (sawDDMM && !sawMMDD) return 'DD/MM/YYYY'
  if (sawMMDD && !sawDDMM) return 'MM/DD/YYYY'
  if (sawIso) return 'YYYY-MM-DD'
  return null
}

export function parseDateOnly(s: string, fmt: DateFormat): string | null {
  const t = s.trim()
  if (!t) return null
  let y: number, m: number, d: number
  if (fmt === 'YYYY-MM-DD') {
    const match = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/)
    if (!match) return null
    y = Number(match[1])
    m = Number(match[2])
    d = Number(match[3])
  } else {
    const match = t.match(/^(\d{1,4})[/\-.](\d{1,4})[/\-.](\d{1,4})$/)
    if (!match) return null
    const a = Number(match[1])
    const b = Number(match[2])
    const c = Number(match[3])
    if (fmt === 'MM/DD/YYYY') {
      m = a
      d = b
      y = c
    } else {
      d = a
      m = b
      y = c
    }
    if (y < 100) y += 2000
  }
  if (m < 1 || m > 12 || d < 1 || d > 31 || y < 2000 || y > 2100) return null
  return `${y.toString().padStart(4, '0')}-${m.toString().padStart(2, '0')}-${d.toString().padStart(2, '0')}`
}

/**
 * Parse a time string into `HH:MM:SS` (24h normalized). Auto-detects
 * 12h vs 24h per-string:
 *   - if the input has an AM/PM suffix → 12h interpretation
 *   - otherwise → 24h interpretation
 *
 * The `fmt` parameter is accepted for back-compat but ignored — the
 * presence/absence of the AM/PM suffix is the only signal the parser
 * needs. (A `13:30` time can't be 12h; a `2:30 PM` time can't be 24h.
 * No realistic Clockify / Toggl export mixes both formats in one
 * column.)
 */
export function parseTimeOnly(
  s: string,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  _fmt?: TimeFormat,
): string | null {
  const t = s.trim()
  if (!t) return null
  const hasMeridiem = /\s*[AaPp][Mm]\s*$/.test(t)
  if (hasMeridiem) {
    const match = t.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s*([AaPp][Mm])$/)
    if (!match) return null
    let h = Number(match[1])
    const m = Number(match[2])
    const sec = match[3] ? Number(match[3]) : 0
    const ampm = match[4].toUpperCase()
    if (h < 1 || h > 12 || m > 59 || sec > 59) return null
    if (ampm === 'PM' && h < 12) h += 12
    if (ampm === 'AM' && h === 12) h = 0
    return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
  }
  // 24h: no AM/PM suffix.
  const match = t.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/)
  if (!match) return null
  const h = Number(match[1])
  const m = Number(match[2])
  const sec = match[3] ? Number(match[3]) : 0
  if (h > 23 || m > 59 || sec > 59) return null
  return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
}

export function joinDateTime(date: string | null, time: string | null): string | null {
  if (!date || !time) return null
  return `${date} ${time}`
}

// Duration formats: "HH:MM:SS", or decimal hours.
export function parseDurationSeconds(s: string): number | null {
  const t = s.trim()
  if (!t) return null
  const hms = t.match(/^(\d+):(\d{2}):(\d{2})$/)
  if (hms) {
    return Number(hms[1]) * 3600 + Number(hms[2]) * 60 + Number(hms[3])
  }
  const hm = t.match(/^(\d+):(\d{2})$/)
  if (hm) {
    return Number(hm[1]) * 3600 + Number(hm[2]) * 60
  }
  const dec = Number(t)
  if (Number.isFinite(dec) && dec >= 0) return Math.round(dec * 3600)
  return null
}

function parseBoolean(s: string): boolean {
  const t = s.trim().toLowerCase()
  return t === 'yes' || t === 'true' || t === '1' || t === 'y'
}

function parseNumberOrNull(s: string): number | null {
  const t = s.trim()
  if (!t) return null
  const n = Number(t)
  return Number.isFinite(n) ? n : null
}

export function parseClockifyCsv(
  csv: string,
  dateFormat: DateFormat,
  timeFormat: TimeFormat,
): ParseResult {
  const parsed = Papa.parse<string[]>(csv.trim(), {
    skipEmptyLines: true,
  })

  const rows = parsed.data
  if (rows.length === 0) {
    return {
      entries: [],
      errors: [{ row: 0, message: 'CSV is empty.' }],
      totalRows: 0,
      uniqueEmails: [],
      dateRange: null,
    }
  }

  const header = rows[0]
  const idx = buildHeaderIndex(header)

  // Billable column is OPTIONAL — Toggl Detailed Report often omits it.
  // Default to true (typical for client time tracking).
  const missing: string[] = []
  for (const required of [
    'project',
    'client',
    'description',
    'email',
    'startDate',
    'startTime',
    'endDate',
    'endTime',
    'duration',
  ] as const) {
    if (idx[required] === -1) missing.push(required)
  }
  if (missing.length) {
    return {
      entries: [],
      errors: [
        {
          row: 0,
          message: `CSV is missing required columns: ${missing.join(', ')}. Make sure you exported with Clockify's default Detailed Report layout.`,
        },
      ],
      totalRows: 0,
      uniqueEmails: [],
      dateRange: null,
    }
  }

  // Auto-detect the date format from the data, falling back to the
  // caller's `dateFormat` argument when every row's dates are
  // ambiguous (e.g., every value has both slots ≤ 12). This makes
  // the form's Date format selector a tiebreaker, not a load-bearing
  // setting. Time format is auto-detected per-string by parseTimeOnly
  // via AM/PM presence; the `timeFormat` arg is now ignored.
  const startDates = rows
    .slice(1)
    .map((r) => r?.[idx.startDate] ?? '')
    .filter((s) => s)
  const detectedDateFormat = detectDateFormat(startDates) ?? dateFormat

  const entries: ParsedEntry[] = []
  const errors: { row: number; message: string }[] = []
  const emails = new Set<string>()
  let minDate: string | null = null
  let maxDate: string | null = null

  for (let i = 1; i < rows.length; i++) {
    const r = rows[i]
    if (!r || r.every((c) => c == null || String(c).trim() === '')) continue

    const sd = parseDateOnly(r[idx.startDate] ?? '', detectedDateFormat)
    const st = parseTimeOnly(r[idx.startTime] ?? '', timeFormat)
    const ed = parseDateOnly(r[idx.endDate] ?? '', detectedDateFormat)
    const et = parseTimeOnly(r[idx.endTime] ?? '', timeFormat)

    if (!sd || !st) {
      errors.push({
        row: i + 1,
        message: `Row ${i + 1}: invalid start date/time (${r[idx.startDate]} ${r[idx.startTime]})`,
      })
      continue
    }

    const start_at = joinDateTime(sd, st)!
    const end_at = joinDateTime(ed, et)

    const duration_seconds = parseDurationSeconds(r[idx.duration] ?? '')
    const email = (r[idx.email] ?? '').trim().toLowerCase()
    const user = (idx.user >= 0 ? r[idx.user] : '').trim()

    const rawProject = (r[idx.project] ?? '').trim()
    const { client: parsedClient, project: parsedProject } =
      splitClientProject(rawProject)
    entries.push({
      source_user_name: user,
      source_user_email: email,
      operator: (r[idx.client] ?? '').trim(),
      client: parsedClient,
      project: parsedProject,
      description: (r[idx.description] ?? '').trim(),
      billable:
        idx.billable >= 0 ? parseBoolean(r[idx.billable] ?? '') : true,
      start_at,
      end_at,
      duration_seconds,
      source_rate_usd: idx.rate >= 0 ? parseNumberOrNull(r[idx.rate]) : null,
      source_amount_usd: idx.amount >= 0 ? parseNumberOrNull(r[idx.amount]) : null,
    })

    if (email) emails.add(email)
    if (!minDate || sd < minDate) minDate = sd
    if (!maxDate || sd > maxDate) maxDate = sd
  }

  return {
    entries,
    errors,
    totalRows: rows.length - 1,
    uniqueEmails: Array.from(emails).sort(),
    dateRange: minDate && maxDate ? { min: minDate, max: maxDate } : null,
  }
}

/**
 * Apply the team_member lookup to compute converted_user and converted_duration_seconds.
 * Returns the fields that should be set on the time_entries row.
 */
export type TeamLookup = Map<
  string,
  {
    id: string
    display_name: string
    /**
     * Legacy field. After the "eliminate proportion" migration this is
     * ONLY used to compute converted_duration_seconds at write time
     * (which feeds the Toggl CSV export). Cost / billout no longer use it.
     */
    rate_proportion: number
    consolidate_as: string | null
    /**
     * Owner-only: per-source-hour cost rate (USD). Replaces the old
     * (team.base_rate × rate_proportion) formula. NULL when not set.
     */
    cost_rate_usd: number | null
    /**
     * Owner-only: per-source-hour client billout rate (USD). NULL when
     * not set; callers leave `time_entries.billout_amount_usd` NULL.
     */
    billout_rate_usd: number | null
  }
>

export function applyConversion(
  entry: ParsedEntry,
  lookup: TeamLookup,
): {
  team_member_id: string | null
  converted_user: string | null
  /** Source duration × member's rate_proportion. Used ONLY for the Toggl
   * CSV export. Cost / billout math no longer reads this. */
  converted_duration_seconds: number | null
  /** Per-source-hour cost rate. Used by the import action to stamp
   * billout_cost_usd. */
  cost_rate_usd: number | null
  /** Per-source-hour billout rate (fallback if no project override). */
  billout_rate_usd: number | null
} {
  const member = lookup.get(entry.source_user_email)
  if (!member) {
    return {
      team_member_id: null,
      converted_user: null,
      converted_duration_seconds: null,
      cost_rate_usd: null,
      billout_rate_usd: null,
    }
  }
  const converted_user = member.consolidate_as ?? member.display_name
  const converted_duration_seconds =
    entry.duration_seconds == null
      ? null
      : Math.round(entry.duration_seconds * member.rate_proportion)
  return {
    team_member_id: member.id,
    converted_user,
    converted_duration_seconds,
    cost_rate_usd: member.cost_rate_usd,
    billout_rate_usd: member.billout_rate_usd,
  }
}

/**
 * Compute the per-entry billout amount given converted-billout hours
 * and the team_member's billout rate. Locked at write time — once
 * stored on the row, rate changes don't retroactively re-cost it
 * (same semantic as billout_cost_usd, ADR #014).
 */
export function computeBilloutAmount(
  convertedDurationSeconds: number | null,
  billoutRateUsd: number | null,
): number | null {
  if (convertedDurationSeconds == null || billoutRateUsd == null) return null
  return Math.round((convertedDurationSeconds / 3600) * billoutRateUsd * 100) / 100
}
