/**
 * Best-effort date detection from a photo's filesystem path.
 *
 * Used by the Date Taken repair flow to suggest a capture date when the
 * stored `taken_at` looks wrong (epoch zero, wildly off, missing). Libraries
 * collected by humans tend to be sorted into dated folders like
 * `2019-07-12_vacation/` or dumped with camera filenames like
 * `IMG_20190712_153045.jpg` — both are stronger signals than a reset EXIF
 * timestamp when the file is clearly misfiled.
 *
 * Pure, deterministic, no I/O. Returns `null` when no recognisable date
 * can be extracted.
 */

export type DateGuessConfidence = 'high' | 'medium' | 'low'
export type DateGuessSource = 'folder' | 'filename'

export interface DateGuess {
  /** The guessed capture date, noon local time for non-specific matches so
   *  timeline-day bucketing isn't ambiguous around midnight. */
  date: Date
  /** How specific the match was — day-level matches are `high`, month-level
   *  `medium`, year-only `low`. */
  confidence: DateGuessConfidence
  /** The substring of the filepath that produced the match — shown in the
   *  UI so the user can sanity-check the guess. */
  matched: string
  /** Whether the date came from the file's basename or an ancestor folder. */
  source: DateGuessSource
}

const MIN_YEAR = 1970
const MAX_YEAR = new Date().getFullYear() + 1

function validYear(y: number): boolean {
  return Number.isInteger(y) && y >= MIN_YEAR && y <= MAX_YEAR
}

function validMonth(m: number): boolean {
  return Number.isInteger(m) && m >= 1 && m <= 12
}

function validDay(d: number): boolean {
  return Number.isInteger(d) && d >= 1 && d <= 31
}

/** Construct a Date at local noon (avoids midnight/timezone rounding
 *  into the previous day when the UI formats to YYYY-MM-DD). Returns
 *  null when the (y, m, d) combo rolls over (e.g. Feb 30). */
function makeDate(y: number, m: number, d: number): Date | null {
  if (!validYear(y) || !validMonth(m) || !validDay(d)) return null
  const dt = new Date(y, m - 1, d, 12, 0, 0, 0)
  if (
    dt.getFullYear() !== y ||
    dt.getMonth() !== m - 1 ||
    dt.getDate() !== d
  ) {
    return null
  }
  return dt
}

/** Split a path into its segments regardless of OS separator. */
function segments(filepath: string): string[] {
  return filepath.split(/[\\/]+/).filter((s) => s.length > 0)
}

/** Run every pattern against a single string and return the strongest
 *  match. Day-level > month-level > year-only; within a tier the first
 *  pattern that fires wins (patterns are written in order of specificity).
 *  `source` is stamped onto the returned guess so the caller can tell
 *  filename hits from folder hits. `allowYearOnly` is off for filenames
 *  to avoid treating a camera serial like `DSC2019` as a year match. */
function guessFromString(
  input: string,
  source: DateGuessSource,
  allowYearOnly: boolean,
): DateGuess | null {
  if (!input) return null

  // Day-level: YYYYMMDD run bounded by non-digits ─ `IMG_20190712_153045`.
  const compact = input.match(/(?<!\d)(\d{4})(\d{2})(\d{2})(?!\d)/)
  if (compact) {
    const date = makeDate(+compact[1], +compact[2], +compact[3])
    if (date) {
      return {
        date,
        confidence: 'high',
        matched: `${compact[1]}-${compact[2]}-${compact[3]}`,
        source,
      }
    }
  }

  // Day-level: YYYY-MM-DD / YYYY_MM_DD / YYYY.MM.DD ─ `2019-07-12_vacation`.
  const dashed = input.match(/(?<!\d)(\d{4})[-_.](\d{1,2})[-_.](\d{1,2})(?!\d)/)
  if (dashed) {
    const date = makeDate(+dashed[1], +dashed[2], +dashed[3])
    if (date) {
      return {
        date,
        confidence: 'high',
        matched: `${dashed[1]}-${dashed[2]}-${dashed[3]}`,
        source,
      }
    }
  }

  // Month-level: YYYY-MM / YYYY_MM ─ snapped to day 15. Requires an
  // explicit separator so a filename digit run doesn't misfire.
  const monthOnly = input.match(/(?<!\d)(\d{4})[-_.](\d{1,2})(?!\d)/)
  if (monthOnly) {
    const date = makeDate(+monthOnly[1], +monthOnly[2], 15)
    if (date) {
      return {
        date,
        confidence: 'medium',
        matched: `${monthOnly[1]}-${monthOnly[2]}`,
        source,
      }
    }
  }

  // Year-only: only enabled for folder segments. A bare year in a
  // filename is too easily confused with a camera serial number.
  if (allowYearOnly) {
    const yearOnly = input.match(/(?<!\d)(\d{4})(?!\d)/)
    if (yearOnly) {
      const date = makeDate(+yearOnly[1], 7, 1)
      if (date) {
        return {
          date,
          confidence: 'low',
          matched: yearOnly[1],
          source,
        }
      }
    }
  }

  return null
}

/** Walk three consecutive path segments looking for `YYYY/MM/DD` or
 *  `YYYY/MM` layouts. These patterns span segment boundaries so the
 *  single-segment scanner above can't see them. */
function guessFromFolderLayout(folders: string[]): DateGuess | null {
  // YYYY / MM / DD — day-level, preferred.
  for (let i = 0; i <= folders.length - 3; i++) {
    const a = folders[i]
    const b = folders[i + 1]
    const c = folders[i + 2]
    if (/^\d{4}$/.test(a) && /^\d{1,2}$/.test(b) && /^\d{1,2}$/.test(c)) {
      const date = makeDate(+a, +b, +c)
      if (date) {
        return {
          date,
          confidence: 'high',
          matched: `${a}/${b}/${c}`,
          source: 'folder',
        }
      }
    }
  }
  // YYYY / MM — month-level.
  for (let i = 0; i <= folders.length - 2; i++) {
    const a = folders[i]
    const b = folders[i + 1]
    if (/^\d{4}$/.test(a) && /^\d{1,2}$/.test(b)) {
      const date = makeDate(+a, +b, 15)
      if (date) {
        return {
          date,
          confidence: 'medium',
          matched: `${a}/${b}`,
          source: 'folder',
        }
      }
    }
  }
  return null
}

const CONFIDENCE_RANK: Record<DateGuessConfidence, number> = {
  high: 3,
  medium: 2,
  low: 1,
}

/**
 * Inspect the filename AND the folder chain for date signals and return
 * the best candidate. **Filename is the source of truth**: if the basename
 * yields any valid match at all, it wins — even a year-only filename hit
 * beats a day-level folder hit. Camera firmwares bake the shutter date
 * into the filename and operators tend to sort photos into broad
 * year/month buckets later, so the filename signal is almost always
 * closer to the real capture date than the folder signal.
 *
 * When the filename has nothing, we fall back to a folder scan:
 * deepest-folder-first for single-segment hits (e.g. `2019-07-12_trip`),
 * then cross-segment layouts (`/2010/07/12/`, `/2010/07/`), then a bare
 * year folder as the weakest last resort.
 */
export function guessDateFromPath(filepath: string): DateGuess | null {
  if (!filepath) return null

  const segs = segments(filepath)
  if (segs.length === 0) return null
  const filename = segs[segs.length - 1]
  const folders = segs.slice(0, -1)

  // Filename is the source of truth: any filename hit (even month-level)
  // wins over anything the folder tree can offer. Year-only is disabled
  // for filenames so camera serials don't masquerade as years.
  const fromFilename = guessFromString(filename, 'filename', false)
  if (fromFilename) return fromFilename

  // Folder fallback: walk deepest-first so a nested dated folder beats
  // an ancestor year folder. First hit wins; we keep walking only if it
  // was weaker than day-level, in case a shallower segment has a
  // stronger match (rare, but e.g. `/archive/2019-07-12/month3/`).
  let bestFolder: DateGuess | null = null
  for (let i = folders.length - 1; i >= 0; i--) {
    const hit = guessFromString(folders[i], 'folder', true)
    if (!hit) continue
    if (!bestFolder || CONFIDENCE_RANK[hit.confidence] > CONFIDENCE_RANK[bestFolder.confidence]) {
      bestFolder = hit
      if (hit.confidence === 'high') break
    }
  }

  // Cross-segment layouts like `/2010/07/12/` can only be found by a
  // multi-segment scanner — try it and keep whichever is stronger.
  const fromLayout = guessFromFolderLayout(folders)
  if (
    fromLayout &&
    (!bestFolder ||
      CONFIDENCE_RANK[fromLayout.confidence] > CONFIDENCE_RANK[bestFolder.confidence])
  ) {
    bestFolder = fromLayout
  }

  return bestFolder
}

/** Format a Date as the `value` of an `<input type="datetime-local">`. */
export function toDatetimeLocalValue(d: Date): string {
  const pad = (n: number) => String(n).padStart(2, '0')
  return (
    `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
    `T${pad(d.getHours())}:${pad(d.getMinutes())}`
  )
}
