#!/usr/bin/env bash

set -euo pipefail

# macOS ships Bash 3.2 and BSD date. Prefer Homebrew's compatible runtime when
# it is installed, even when this script was started explicitly with /bin/bash.
for coreutils_path in \
  /opt/homebrew/opt/coreutils/libexec/gnubin \
  /usr/local/opt/coreutils/libexec/gnubin; do
  if [[ -d "$coreutils_path" ]]; then
    PATH="$coreutils_path:$PATH"
    export PATH
    break
  fi
done
unset coreutils_path

if ((BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3))); then
  for modern_bash in /opt/homebrew/bin/bash /usr/local/bin/bash; do
    if [[ -x "$modern_bash" ]]; then
      exec "$modern_bash" "$0" "$@"
    fi
  done
  printf 'Error: Bash 4.3 or newer is required (found %s).\n' "$BASH_VERSION" >&2
  printf 'On macOS, install it with: brew install bash coreutils\n' >&2
  exit 2
fi
unset modern_bash

readonly STATE_URL="https://mahgit.ir/MeghdadFadaee/daily-blackout-check/raw/branch/main/state/outages.json"
readonly TEHRAN_TZ="Asia/Tehran"

STATE_PATH=""
DOWNLOAD_PATH=""
REFRESH_STATUS_PATH=""
REFRESH_PID=""
declare -a SCHEDULE=()
HERO=""
REFRESH_ERROR=""
TERMINAL_ACTIVE=0

cleanup() {
  if ((TERMINAL_ACTIVE)); then
    printf '\033[?25h\033[?1049l'
  fi
  if [[ -n "$REFRESH_PID" ]]; then
    kill "$REFRESH_PID" 2>/dev/null || true
    wait "$REFRESH_PID" 2>/dev/null || true
  fi
  if [[ -n "$STATE_PATH" && -f "$STATE_PATH" ]]; then
    rm -f -- "$STATE_PATH"
  fi
  if [[ -n "$DOWNLOAD_PATH" && -f "$DOWNLOAD_PATH" ]]; then
    rm -f -- "$DOWNLOAD_PATH"
  fi
  if [[ -n "$REFRESH_STATUS_PATH" && -f "$REFRESH_STATUS_PATH" ]]; then
    rm -f -- "$REFRESH_STATUS_PATH"
  fi
}
trap cleanup EXIT

usage() {
  cat <<'EOF'
Usage: ./main.sh [-i MINUTES] [-b BILL_ID]

Continuously display current and upcoming electricity outages from the public
repository state. The countdown updates every second.

Options:
  -i, --interval MINUTES  Refetch state at this interval (default: 10)
  -b, --bill-id BILL_ID   Use only this bill for the large countdown
  -h, --help              Show this help

Environment:
  NO_COLOR    Disable ANSI colors when set
  COLUMNS     Override the detected output width

Required commands: Bash 4.3+, curl, jq, and GNU date
EOF
}

die() {
  printf 'Error: %s\n' "$*" >&2
  exit 1
}

require_command() {
  local command_name="$1"

  if ! command -v "$command_name" >/dev/null 2>&1; then
    die "missing required command: $command_name"
  fi
}

fetch_state() {
  local destination="$1"
  local cache_key="$2"

  curl -fsSL --compressed \
    --connect-timeout 10 \
    --max-time 30 \
    -H 'Cache-Control: no-cache' \
    -o "$destination" \
    "${STATE_URL}?cli=${cache_key}"
}

start_state_refresh() {
  local cache_key="$1"

  : >"$REFRESH_STATUS_PATH"
  (
    if ! fetch_state "$DOWNLOAD_PATH" "$cache_key" 2>/dev/null; then
      printf 'download-error\n' >"$REFRESH_STATUS_PATH"
      exit 1
    fi
    if ! validate_state "$DOWNLOAD_PATH"; then
      printf 'validation-error\n' >"$REFRESH_STATUS_PATH"
      exit 1
    fi
    printf 'ok\n' >"$REFRESH_STATUS_PATH"
  ) &
  REFRESH_PID="$!"
}

finish_state_refresh() {
  local result

  if [[ -z "$REFRESH_PID" || ! -s "$REFRESH_STATUS_PATH" ]]; then
    return 1
  fi
  wait "$REFRESH_PID" 2>/dev/null || true
  REFRESH_PID=""
  result="$(<"$REFRESH_STATUS_PATH")"

  case "$result" in
    ok)
      if ! cp -- "$DOWNLOAD_PATH" "$STATE_PATH"; then
        REFRESH_ERROR="Could not cache the downloaded state."
        return 1
      fi
      REFRESH_ERROR=""
      return 0
      ;;
    validation-error)
      REFRESH_ERROR="Downloaded state is malformed; showing the last valid state."
      ;;
    *)
      REFRESH_ERROR="State refresh failed; showing the last valid state."
      ;;
  esac

  return 1
}

validate_state() {
  local path="$1"

  jq -e '
    type == "object"
    and .schema_version == 1
    and (.bills | type == "object")
    and all(
      .bills | to_entries[];
      (.key | type == "string")
      and (.value | type == "object")
      and (.value.events | type == "object")
      and all(
        .value.events | to_entries[];
        (.key | type == "string")
        and (.value | type == "object")
        and (.value.status == "active" or .value.status == "cancelled")
        and (.value.snapshot | type == "object")
        and (.value.snapshot.outage_number | type == "string")
        and .value.snapshot.outage_number == .key
        and (.value.snapshot.outage_date | type == "string")
        and (.value.snapshot.start_time | type == "string")
        and (.value.snapshot.stop_time | type == "string")
        and (.value.snapshot.address | type == "string")
        and (.value.snapshot.reason | type == "string")
        and (.value.snapshot.is_planned | type == "boolean")
      )
    )
  ' "$path" >/dev/null 2>&1
}

# Convert a Jalali YYYY/MM/DD date to Gregorian YYYY-MM-DD. This arithmetic
# conversion covers the modern Jalali range used by the outage service.
jalali_to_gregorian() {
  local input="$1"
  local jy jm jd days gy gd gm leap index
  local -a month_days=(0 31 28 31 30 31 30 31 31 30 31 30 31)

  if [[ ! "$input" =~ ^([0-9]{4})/([0-9]{2})/([0-9]{2})$ ]]; then
    return 1
  fi

  jy=$((10#${BASH_REMATCH[1]}))
  jm=$((10#${BASH_REMATCH[2]}))
  jd=$((10#${BASH_REMATCH[3]}))
  if ((jy < 1 || jm < 1 || jm > 12 || jd < 1)); then
    return 1
  fi
  if ((jm <= 6 && jd > 31)); then
    return 1
  fi
  if ((jm >= 7 && jd > 30)); then
    return 1
  fi

  jy=$((jy + 1595))
  days=$((-355668 + (365 * jy) + ((jy / 33) * 8) + (((jy % 33) + 3) / 4) + jd))
  if ((jm < 7)); then
    days=$((days + ((jm - 1) * 31)))
  else
    days=$((days + ((jm - 7) * 30) + 186))
  fi

  gy=$((400 * (days / 146097)))
  days=$((days % 146097))
  if ((days > 36524)); then
    days=$((days - 1))
    gy=$((gy + (100 * (days / 36524))))
    days=$((days % 36524))
    if ((days >= 365)); then
      days=$((days + 1))
    fi
  fi

  gy=$((gy + (4 * (days / 1461))))
  days=$((days % 1461))
  if ((days > 365)); then
    gy=$((gy + ((days - 1) / 365)))
    days=$(((days - 1) % 365))
  fi

  gd=$((days + 1))
  leap=0
  if (((gy % 4 == 0 && gy % 100 != 0) || gy % 400 == 0)); then
    leap=1
  fi
  month_days[2]=$((28 + leap))

  gm=1
  for index in {1..12}; do
    if ((gd <= month_days[index])); then
      gm=$index
      break
    fi
    gd=$((gd - month_days[index]))
  done

  printf '%04d-%02d-%02d\n' "$gy" "$gm" "$gd"
}

event_epochs() {
  local outage_date="$1"
  local start_time="$2"
  local stop_time="$3"
  local gregorian start_epoch stop_epoch

  if [[ ! "$start_time" =~ ^([0-9]{2}):([0-9]{2})$ ]] \
    || ((10#${BASH_REMATCH[1]} > 23 || 10#${BASH_REMATCH[2]} > 59)); then
    return 1
  fi
  if [[ ! "$stop_time" =~ ^([0-9]{2}):([0-9]{2})$ ]] \
    || ((10#${BASH_REMATCH[1]} > 23 || 10#${BASH_REMATCH[2]} > 59)); then
    return 1
  fi

  gregorian="$(jalali_to_gregorian "$outage_date")" || return 1
  start_epoch="$(TZ="$TEHRAN_TZ" date -d "$gregorian $start_time:00" +%s 2>/dev/null)" \
    || return 1
  stop_epoch="$(TZ="$TEHRAN_TZ" date -d "$gregorian $stop_time:00" +%s 2>/dev/null)" \
    || return 1
  if ((stop_epoch <= start_epoch)); then
    stop_epoch=$((stop_epoch + 86400))
  fi

  printf '%s %s\n' "$start_epoch" "$stop_epoch"
}

event_records() {
  local path="$1"

  jq -c '
    .bills
    | to_entries[] as $bill
    | $bill.value.events
    | to_entries[]
    | {
        bill_id: $bill.key,
        outage_number: .key,
        status: .value.status,
        outage_date: .value.snapshot.outage_date,
        start_time: .value.snapshot.start_time,
        stop_time: .value.snapshot.stop_time,
        address: .value.snapshot.address,
        reason: .value.snapshot.reason,
        is_planned: .value.snapshot.is_planned
      }
  ' "$path"
}

record_fields() {
  local record="$1"
  local -n output="$2"

  mapfile -d '' -t output < <(
    jq -j '
      [
        .bill_id,
        .outage_number,
        .status,
        .outage_date,
        .start_time,
        .stop_time,
        .address,
        .reason,
        (.is_planned | tostring)
      ][] | ., "\u0000"
    ' <<<"$record"
  )
}

build_schedule() {
  local path="$1"
  local now_epoch="$2"
  local countdown_bill_id="${3:-}"
  local record start_epoch stop_epoch phase phase_order sort_key line hero_key=""
  local -a fields sortable=()

  SCHEDULE=()
  HERO=""

  while IFS= read -r record; do
    record_fields "$record" fields
    if [[ "${fields[2]}" != "active" ]]; then
      continue
    fi
    if ! read -r start_epoch stop_epoch < <(
      event_epochs "${fields[3]}" "${fields[4]}" "${fields[5]}"
    ); then
      printf 'Invalid outage date or time for event %s\n' "${fields[1]}" >&2
      return 1
    fi
    if ((stop_epoch <= now_epoch)); then
      continue
    fi

    if ((start_epoch <= now_epoch)); then
      phase="ONGOING"
      phase_order=0
      sort_key="$(printf '0:%020d:%s:%s' "$start_epoch" "${fields[0]}" "${fields[1]}")"
      if [[ (-z "$countdown_bill_id" || "${fields[0]}" == "$countdown_bill_id") \
        && (-z "$hero_key" || "$stop_epoch" -lt "$hero_key") ]]; then
        hero_key="$stop_epoch"
        HERO="$phase"$'\t'"$start_epoch"$'\t'"$stop_epoch"$'\t'"$record"
      fi
    else
      phase="UPCOMING"
      phase_order=1
      sort_key="$(printf '1:%020d:%s:%s' "$start_epoch" "${fields[0]}" "${fields[1]}")"
      if [[ (-z "$countdown_bill_id" || "${fields[0]}" == "$countdown_bill_id") \
        && -z "$hero_key" && -z "$HERO" ]]; then
        HERO="$phase"$'\t'"$start_epoch"$'\t'"$stop_epoch"$'\t'"$record"
      elif [[ (-z "$countdown_bill_id" || "${fields[0]}" == "$countdown_bill_id") \
        && "${HERO%%$'\t'*}" == "UPCOMING" ]]; then
        local current_hero_start
        current_hero_start="${HERO#*$'\t'}"
        current_hero_start="${current_hero_start%%$'\t'*}"
        if ((start_epoch < current_hero_start)); then
          HERO="$phase"$'\t'"$start_epoch"$'\t'"$stop_epoch"$'\t'"$record"
        fi
      fi
    fi
    line="$sort_key"$'\t'"$phase"$'\t'"$start_epoch"$'\t'"$stop_epoch"$'\t'"$record"
    sortable+=("$line")
  done < <(event_records "$path")

  if ((${#sortable[@]})); then
    mapfile -t SCHEDULE < <(printf '%s\n' "${sortable[@]}" | LC_ALL=C sort)
  fi
}

terminal_columns() {
  local columns="${COLUMNS:-}"
  local terminal_size="" detected_columns=""

  if [[ -z "$columns" ]] && command -v stty >/dev/null 2>&1; then
    if [[ -r /dev/tty ]]; then
      terminal_size="$(stty size 2>/dev/null </dev/tty || true)"
    fi
    if [[ -z "$terminal_size" && -t 2 ]]; then
      terminal_size="$(stty size <&2 2>/dev/null || true)"
    fi
    if [[ -z "$terminal_size" && -t 0 ]]; then
      terminal_size="$(stty size <&0 2>/dev/null || true)"
    fi
    read -r _ detected_columns <<<"$terminal_size"
    if [[ "$detected_columns" =~ ^[0-9]+$ ]]; then
      columns="$detected_columns"
    fi
  fi

  if [[ -z "$columns" ]] && command -v tput >/dev/null 2>&1; then
    columns="$(tput cols 2>/dev/null || true)"
  fi

  if [[ ! "$columns" =~ ^[0-9]+$ || "$columns" -lt 40 ]]; then
    columns=80
  fi
  printf '%s\n' "$columns"
}

color_setup() {
  RESET=""
  BOLD=""
  DIM=""
  CYAN=""
  GREEN=""
  YELLOW=""
  RED=""

  if ((TERMINAL_ACTIVE)) && [[ -z "${NO_COLOR+x}" ]]; then
    RESET=$'\033[0m'
    BOLD=$'\033[1m'
    DIM=$'\033[2m'
    CYAN=$'\033[36m'
    GREEN=$'\033[32m'
    YELLOW=$'\033[33m'
    RED=$'\033[31m'
  fi
}

center_line() {
  local text="$1"
  local columns="$2"
  local padding=0

  if ((${#text} < columns)); then
    padding=$(((columns - ${#text}) / 2))
  fi
  printf '%*s%s\n' "$padding" '' "$text"
}

repeat_character() {
  local character="$1"
  local count="$2"
  local output=""

  printf -v output '%*s' "$count" ''
  printf '%s' "${output// /$character}"
}

large_glyph() {
  case "$1" in
    0)
      cat <<'EOF'
   ,a8888a,
 ,8P"'  `"Y8,
,8P        Y8,
88          88
88          88
`8b        d8'
 `8ba,  ,ad8'
   "Y8888P"
EOF
      ;;
    1)
      cat <<'EOF'
    88
  ,d88
888888
    88
    88
    88
    88
    88
EOF
      ;;
    2)
      cat <<'EOF'
 ad888888b,
d8"     "88
        a8P
     ,d8P"
   a8P"
 a8P'
d8"
88888888888
EOF
      ;;
    3)
      cat <<'EOF'
 ad888888b,
d8"     "88
        a8P
     aad8"
     ""Y8,
        "8b
Y8,     a88
 "Y888888P'
EOF
      ;;
    4)
      cat <<'EOF'
        ,d8
      ,d888
    ,d8" 88
  ,d8"   88
,d8"     88
8888888888888
         88
         88
EOF
      ;;
    5)
      cat <<'EOF'
8888888888
88
88  ____
88a8PPPP8b,
PP"     `8b
         d8
Y8a     a8P
 "Y88888P"
EOF
      ;;
    6)
      cat <<'EOF'
  ad8888ba,
 8P'    "Y8
d8
88,dd888bb,
88P'    `8b
88       d8
88a     a8P
 "Y88888P"
EOF
      ;;
    7)
      cat <<'EOF'
888888888888
        ,8P'
       d8"
     ,8P'
    d8"
  ,8P'
 d8"
8P'
EOF
      ;;
    8)
      cat <<'EOF'
 ad88888ba
d8"     "8b
Y8a     a8P
 "Y8aaa8P"
 ,d8"""8b,
d8"     "8b
Y8a     a8P
 "Y88888P"
EOF
      ;;
    9)
      cat <<'EOF'
 ad88888ba
d8"     "88
8P       88
Y8,    ,d88
 "PPPPPP"88
         8P
8b,    a8P
`"Y8888P'
EOF
      ;;
    D)
      cat <<'EOF'
88888888ba
88      "8b
88       8b
88       88
88       88
88       8P
88      a8P
88888888P"
EOF
      ;;
    H)
      cat <<'EOF'
88      88
88      88
88      88
8888888888
88      88
88      88
88      88
88      88
EOF
      ;;
    M)
      cat <<'EOF'
88b           d88
888b         d888
88`8b       d8'88
88 `8b     d8' 88
88  `8b   d8'  88
88   `8b d8'   88
88    `888'    88
88     `8'     88
EOF
      ;;
    :)
      printf '%s\n' '   ' '888' '888' '   ' '   ' '888' '888' '   '
      ;;
    ' ')
      printf '%s\n' '    ' '    ' '    ' '    ' '    ' '    ' '    ' '    '
      ;;
    *)
      printf '%s\n' '???' '???' '???' '???' '???' '???' '???' '???'
      ;;
  esac
}

print_large_countdown() {
  local text="$1"
  local columns="$2"
  local rendered_banner
  local character glyph_line glyph_width index row rendered_width=0 block_padding=0
  local -a glyph_lines rows=('' '' '' '' '' '' '' '')

  if command -v pyfiglet >/dev/null 2>&1; then
    if rendered_banner="$(pyfiglet -f univers -w "$columns" -j center "$text" 2>/dev/null)"; then
      printf '%s\n' "$rendered_banner"
      return
    fi
  fi
  if command -v figlet >/dev/null 2>&1; then
    if rendered_banner="$(figlet -f univers -w "$columns" -c "$text" 2>/dev/null)"; then
      printf '%s\n' "$rendered_banner"
      return
    fi
  fi

  for ((index = 0; index < ${#text}; index++)); do
    character="${text:index:1}"
    mapfile -t glyph_lines < <(large_glyph "$character")
    glyph_width=0
    for row in {0..7}; do
      if ((${#glyph_lines[row]} > glyph_width)); then
        glyph_width="${#glyph_lines[row]}"
      fi
    done
    for row in {0..7}; do
      printf -v glyph_line '%-*s' "$glyph_width" "${glyph_lines[row]:-}"
      rows[row]+="$glyph_line "
    done
  done

  for row in {0..7}; do
    while [[ "${rows[row]}" == *' ' ]]; do
      rows[row]="${rows[row]% }"
    done
    if ((${#rows[row]} > rendered_width)); then
      rendered_width="${#rows[row]}"
    fi
  done

  if ((rendered_width > columns)); then
    center_line "$text" "$columns"
    return
  fi

  block_padding=$(((columns - rendered_width) / 2))
  for row in {0..7}; do
    printf '%*s%s\n' "$block_padding" '' "${rows[row]}"
  done
}

duration_parts() {
  local seconds="$1"
  local minutes days hours

  ((seconds < 0)) && seconds=0
  minutes=$(((seconds + 59) / 60))
  days=$((minutes / 1440))
  hours=$(((minutes % 1440) / 60))
  minutes=$((minutes % 60))
  printf '%s %s %s\n' "$days" "$hours" "$minutes"
}

banner_duration() {
  local seconds="$1"
  local minutes hours

  ((seconds < 0)) && seconds=0
  hours=$((seconds / 3600))
  minutes=$(((seconds % 3600) / 60))
  seconds=$((seconds % 60))
  printf '%02d : %02d : %02d\n' "$hours" "$minutes" "$seconds"
}

human_duration() {
  local seconds="$1"
  local days hours minutes output=""

  read -r days hours minutes < <(duration_parts "$seconds")
  ((days)) && output+="${days}d "
  ((hours)) && output+="${hours}h "
  output+="${minutes}m"
  printf '%s\n' "$output"
}

render_event() {
  local line="$1"
  local now_epoch="$2"
  local ignored phase start_epoch stop_epoch record type_label timing
  local -a fields

  IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$line"
  record_fields "$record" fields
  type_label="EMERGENCY"
  [[ "${fields[8]}" == "true" ]] && type_label="PLANNED"

  if [[ "$phase" == "ONGOING" ]]; then
    timing="ends in $(human_duration "$((stop_epoch - now_epoch))")"
  else
    timing="starts in $(human_duration "$((start_epoch - now_epoch))")"
  fi

  printf '%s%s%-10s%s  %s%-9s%s  %s\n' \
    "$BOLD" "$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \
    "$phase" "$RESET" "$([[ "$type_label" == "PLANNED" ]] && printf '%s' "$GREEN" || printf '%s' "$YELLOW")" \
    "$type_label" "$RESET" "$timing"
  printf '  Date       %s   %s - %s\n' "${fields[3]}" "${fields[4]}" "${fields[5]}"
  printf '  Reference  bill %s / outage %s\n' "${fields[0]}" "${fields[1]}"
}

render_empty() {
  local columns="$1"
  local rule

  rule="$(repeat_character '=' "$columns")"
  printf '%s%s%s\n' "$GREEN" "$rule" "$RESET"
  printf '\n'
  center_line "ALL CLEAR" "$columns"
  center_line "No current or upcoming outages." "$columns"
  printf '\n'
  printf '%s%s%s\n' "$GREEN" "$rule" "$RESET"
}

render_unavailable() {
  local columns="${1:-}"
  local rule

  color_setup
  if [[ -z "$columns" ]]; then
    columns="$(terminal_columns)"
  fi
  rule="$(repeat_character '=' "$columns")"
  printf '%s%s%s\n\n' "$RED" "$rule" "$RESET"
  center_line "STATE UNAVAILABLE" "$columns"
  center_line "Waiting for the next refresh." "$columns"
  printf '\n%s%s%s\n' "$RED" "$rule" "$RESET"
}

start_terminal() {
  if [[ -t 1 ]]; then
    TERMINAL_ACTIVE=1
    printf '\033[?1049h\033[?25l\033[H'
  fi
}

draw_frame() {
  local frame="$1"
  local cleared_frame

  if ((TERMINAL_ACTIVE)); then
    cleared_frame="${frame//$'\n'/$'\033[K\n'}"
    printf '\033[H%s\033[K\n\033[J' "$cleared_frame"
  else
    printf '%s\n' "$frame"
  fi
}

schedule_next_transition() {
  local now_epoch="$1"
  local line ignored phase start_epoch stop_epoch record candidate=0

  for line in "${SCHEDULE[@]}"; do
    IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$line"
    if ((start_epoch > now_epoch)) && ((candidate == 0 || start_epoch < candidate)); then
      candidate="$start_epoch"
    fi
    if ((stop_epoch > now_epoch)) && ((candidate == 0 || stop_epoch < candidate)); then
      candidate="$stop_epoch"
    fi
  done
  printf '%s\n' "$candidate"
}

render_dashboard() {
  local now_epoch="$1"
  local columns="${2:-}"
  local countdown_bill_id="${3:-}"
  local rule phase start_epoch stop_epoch record remaining banner
  local schedule_line ignored

  color_setup
  if [[ -z "$columns" ]]; then
    columns="$(terminal_columns)"
  fi

  if ((${#SCHEDULE[@]} == 0)); then
    render_empty "$columns"
    return
  fi

  rule="$(repeat_character '=' "$columns")"
  printf '%s%s%s\n' "$CYAN" "$rule" "$RESET"
  printf '\n'

  if [[ -z "$HERO" ]]; then
    center_line "NO CURRENT OR UPCOMING OUTAGE" "$columns"
    center_line "Countdown bill $countdown_bill_id" "$columns"
  else
    IFS=$'\t' read -r phase start_epoch stop_epoch record <<<"$HERO"
    if [[ "$phase" == "ONGOING" ]]; then
      remaining=$((stop_epoch - now_epoch))
    else
      remaining=$((start_epoch - now_epoch))
    fi
    banner="$(banner_duration "$remaining")"
    if [[ -n "$countdown_bill_id" ]]; then
      center_line "COUNTDOWN FOR BILL $countdown_bill_id" "$columns"
      printf '\n'
    fi
    printf '%s%s' "$BOLD" "$YELLOW"
    print_large_countdown "$banner" "$columns"
    printf '%s' "$RESET"
  fi

  printf '\n%s%s%s\n' "$CYAN" "$rule" "$RESET"
  printf '\n%sOUTAGE SCHEDULE (%d)%s\n\n' "$BOLD" "${#SCHEDULE[@]}" "$RESET"

  for schedule_line in "${SCHEDULE[@]}"; do
    IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$schedule_line"
    render_event "$schedule_line" "$now_epoch"
    printf '%s\n' "$DIM$(repeat_character '-' "$columns")$RESET"
  done
}

main() {
  local now_epoch next_refresh=0 refresh_minutes=10 refresh_seconds state_ready=0
  local last_rendered=-1 next_transition=0 rebuild_schedule=0 frame key input_fd=0
  local display_columns countdown_bill_id=""

  while (($#)); do
    case "$1" in
      -h|--help)
        usage
        return 0
        ;;
      -i|--interval)
        if (($# < 2)); then
          printf 'Error: %s requires a value\n' "$1" >&2
          usage >&2
          return 2
        fi
        refresh_minutes="$2"
        shift 2
        ;;
      --interval=*)
        refresh_minutes="${1#*=}"
        shift
        ;;
      -b|--bill-id)
        if (($# < 2)); then
          printf 'Error: %s requires a value\n' "$1" >&2
          usage >&2
          return 2
        fi
        countdown_bill_id="$2"
        shift 2
        ;;
      --bill-id=*)
        countdown_bill_id="${1#*=}"
        shift
        ;;
      *)
        printf 'Error: unknown option: %s\n' "$1" >&2
        usage >&2
        return 2
        ;;
    esac
  done
  if [[ ! "$refresh_minutes" =~ ^[0-9]+$ ]] \
    || ((10#$refresh_minutes < 1 || 10#$refresh_minutes > 1440)); then
    printf 'Error: interval must be a whole number from 1 to 1440 minutes\n' >&2
    return 2
  fi
  if [[ -n "$countdown_bill_id" && ! "$countdown_bill_id" =~ ^[0-9]+$ ]]; then
    printf 'Error: bill ID must contain only decimal digits\n' >&2
    return 2
  fi
  refresh_seconds=$((10#$refresh_minutes * 60))

  require_command curl
  require_command jq
  require_command date

  STATE_PATH="$(mktemp "${TMPDIR:-/tmp}/blackout-state.XXXXXX")"
  DOWNLOAD_PATH="$(mktemp "${TMPDIR:-/tmp}/blackout-download.XXXXXX")"
  REFRESH_STATUS_PATH="$(mktemp "${TMPDIR:-/tmp}/blackout-refresh.XXXXXX")"
  if [[ -r /dev/tty ]]; then
    exec {input_fd}</dev/tty
  fi
  start_terminal

  while true; do
    if [[ -t "$input_fd" ]]; then
      key=""
      if read -u "$input_fd" -rsn 1 -t 0.1 key \
        && [[ "$key" == "q" || "$key" == "Q" ]]; then
        break
      fi
    fi

    printf -v now_epoch '%(%s)T' -1
    if ((now_epoch == last_rendered)); then
      if [[ ! -t "$input_fd" ]]; then
        sleep 0.1
      fi
      continue
    fi
    last_rendered="$now_epoch"

    if ((now_epoch >= next_refresh)) && [[ -z "$REFRESH_PID" ]]; then
      start_state_refresh "$now_epoch"
      next_refresh=$((now_epoch + refresh_seconds))
    fi
    if [[ -n "$REFRESH_PID" && -s "$REFRESH_STATUS_PATH" ]]; then
      if finish_state_refresh; then
        state_ready=1
        rebuild_schedule=1
      fi
    fi

    if ((state_ready)); then
      if ((rebuild_schedule || (next_transition > 0 && now_epoch >= next_transition))); then
        if build_schedule "$STATE_PATH" "$now_epoch" "$countdown_bill_id"; then
          next_transition="$(schedule_next_transition "$now_epoch")"
          rebuild_schedule=0
        else
          state_ready=0
          REFRESH_ERROR="The cached state contains an invalid date or time."
        fi
      fi
    fi

    display_columns="$(terminal_columns)"
    frame="$(
      if ((state_ready)); then
        render_dashboard "$now_epoch" "$display_columns" "$countdown_bill_id"
      else
        render_unavailable "$display_columns"
      fi
      if [[ -n "$REFRESH_ERROR" ]]; then
        printf '\n%s\n' "$REFRESH_ERROR"
      fi
    )"
    draw_frame "$frame"
  done
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
  main "$@"
fi
