#!/bin/bash
# content-planner: контент-планёрка → телеграм (токен tasks-bot)
#
# Один прогон в неделю: вс 18:00 Екб — скелет СЛЕДУЮЩЕЙ недели и темы на все
# семь дней.
#
# Раньше их было два: воскресенье давало темы на пн–ср, а среда дописывала
# чт–вс, потому что тема живёт трое суток. Свежесть теперь держит утренний
# прогон (morning.sh, ежедневно 8:15): он сверяет тему сегодняшнего дня со
# всем, что появилось за сутки, и при сдвиге вектора поднимает в строку один
# из вариантов, которые этот прогон положил под полями дня. Ежедневная сверка
# перекрывает среду, поэтому среда выпилена (29.07.2026).
#
# Кроссплатформенный (Mac launchd / Pi systemd). Пути из .env:
#   VAULT_ROOT (дефолт — Mac-волт), PLANNER_DATA_DIR (логи), PLANNER_GIT_SYNC=1 (Pi)
# Бэклог идей живёт в волте (projects/channel/planner/), путь зашит в промптах
# Запуск: bash planerka.sh   (аргументов нет)
set -u

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# Режимов больше нет, но мышечная память есть — и у Наташи, и в чужих юнитах.
# Молча проглотить аргумент значило бы сделать вид, что среда отработала.
if [ "${1:-}" = "refresh" ]; then
  echo "режима refresh больше нет: темы на всю неделю ставит воскресный прогон," >&2
  echo "а свежесть держит утренний (morning.sh, ежедневно 8:15)." >&2
  exit 2
fi
PROMPT_FILE="$SCRIPT_DIR/prompt.md"

# --- секреты (телеграм tasks-bot + TickTick) и пути
set -a; source "$SCRIPT_DIR/.env"; set +a
VAULT="${VAULT_ROOT:-/Users/nataliadudina/Desktop/ObsidianVault}"
LOG_DIR="${PLANNER_DATA_DIR:-$HOME/Library/Application Support/content-planner}"
LOG="$LOG_DIR/run.log"
# Каталог обмена с tasks-bot: сюда кладётся всё, что забирает утренняя
# доставка (блоки дня от morning.sh и ссылка на план недели отсюда).
# Логи и служебное состояние живут уровнем выше, в $LOG_DIR, — тем и
# отличаются: обмен читает чужой процесс, состояние — только этот скрипт.
MORNING_DIR="${MORNING_CONTENT_DIR:-$LOG_DIR/morning}"
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude)}"
mkdir -p "$LOG_DIR" "$MORNING_DIR"

# Общие с morning.sh: log, tg_send, tg_notify, vault_pull, vault_push,
# ticktick_ideas. Переменные выше — их вход, поэтому source идёт после них.
NOTIFY_PREFIX="планёрка"
source "$SCRIPT_DIR/lib.sh"

if [ -z "${TELEGRAM_BOT_TOKEN:-}" ] || [ -z "${TELEGRAM_OWNER_USER_ID:-}" ]; then
  log "ERROR: нет токена/chat_id в .env"; exit 1
fi

# --- аварийный сигнал: короткое сообщение в ТГ, если прогон падает.
# Отдельно от tg_notify только ради ALERTED: по нему трап на выходе понимает,
# сказали мы уже вслух или падаем молча.
ALERTED=0
alert() {
  ALERTED=1
  tg_notify "упала: $1"
}
# страховка: любой неожиданный exit с ненулевым кодом, который мы сами не обработали
trap 'rc=$?; if [ "$rc" -ne 0 ] && [ "$ALERTED" = 0 ]; then alert "неожиданный сбой (rc=$rc), смотри run.log"; fi' EXIT

# --- на Pi: подтянуть свежий волт перед прогоном
vault_pull

# --- предполётная проверка источников
# Планёрка читает файлы молча: нет архива или отчёта ниши — просто спланирует
# без них, и план будет выглядеть нормальным. Тихая потеря данных хуже сбоя,
# поэтому предупреждаем сразу. Планёрку при этом не отменяем: неполный план
# полезнее, чем никакого.
PREFLIGHT=$(python3 "$SCRIPT_DIR/preflight.py" "$VAULT" 2>>"$LOG")
if [ -n "$PREFLIGHT" ]; then
  log "preflight: $(echo "$PREFLIGHT" | tr '\n' ';')"
  tg_notify "источники не в порядке
$PREFLIGHT

План всё равно будет, но с дырами."
else
  log "preflight: источники на месте"
fi

# --- окно недели: планируем следующую
TODAY=$(date +%Y-%m-%d)
read -r WEEK_START WEEK_END <<<"$(python3 "$SCRIPT_DIR/weekdates.py" "$TODAY")"
if [ -z "${WEEK_START:-}" ]; then
  log "ERROR: не посчитались даты недели"; alert "не посчитались даты недели"; exit 1
fi

# --- прирост канала и упоминания (TGStat → накопительный файл в волте)
# Копим сами: API отдаёт только ~10 дней истории подписчиков и ~30 дней
# упоминаний, дальше данные пропадают навсегда. Планёрка бегает дважды в неделю —
# окна хватает с запасом, даже если один прогон упал. Сбой сбора планёрку не
# отменяет: она прочитает прошлый срез, просто менее свежий.
GROWTH=$(python3 "$SCRIPT_DIR/growth.py" "$VAULT" "$TODAY" 2>>"$LOG")
if [ -n "$GROWTH" ]; then
  log "growth: $GROWTH"
else
  log "WARN: growth не собрался — планёрка пойдёт на прошлом срезе"
fi
WEEKLY_DIR="$VAULT/projects/channel/planner"
WEEKLY_FILE="$WEEKLY_DIR/{channel} {plan} неделя – $WEEK_START.md"
TAKEN_FILE="$WEEKLY_DIR/.taken-$WEEK_START"
mkdir -p "$WEEKLY_DIR"
rm -f "$TAKEN_FILE"

# --- идеи Наташи из TickTick (Channel-списки; To Do не трогаем — там настоящие задачи)
# Списки живут в .env: их читает ещё и morning.sh, а два захардкоженных
# набора id молча разъезжаются при первом же переименовании списка.
# Берём весь список без окна свежести: раз в неделю уместно показать всё,
# что накопилось. Маркеры pid/id нужны, чтобы потом закрыть взятое.
IDEAS=$(ticktick_ideas "" markers)
if [ -z "$IDEAS" ]; then
  IDEAS="(идей в TickTick сейчас нет)"
fi
log "ticktick: собрано идей: $(printf '%s' "$IDEAS" | grep -c '^- ' || true)"

# --- собрать промпт из шаблона (идеи подставляем через файл, чтобы не ломать sed спецсимволами)
IDEAS_TMP=$(mktemp)
printf '%s\n' "$IDEAS" > "$IDEAS_TMP"
PROMPT=$(python3 - "$PROMPT_FILE" "$IDEAS_TMP" <<PYEOF
import sys
tpl = open(sys.argv[1]).read()
ideas = open(sys.argv[2]).read()
out = (tpl.replace('{{TODAY}}', '$TODAY')
          .replace('{{WEEK_START}}', '$WEEK_START')
          .replace('{{WEEK_END}}', '$WEEK_END')
          .replace('{{WEEKLY_FILE}}', '''$WEEKLY_FILE''')
          .replace('{{TAKEN_FILE}}', '''$TAKEN_FILE''')
          .replace('{{IDEAS}}', ideas))
print(out)
PYEOF
)
rm -f "$IDEAS_TMP"

log "start: неделя $WEEK_START – $WEEK_END"

# --- headless Claude в корне волта
SUMMARY_FILE=$(mktemp)
cd "$VAULT" || { log "ERROR: волт недоступен"; alert "волт недоступен ($VAULT)"; exit 1; }
"$CLAUDE_BIN" -p "$PROMPT" \
  --model claude-opus-5 \
  --permission-mode acceptEdits \
  --output-format text \
  > "$SUMMARY_FILE" 2>>"$LOG"
RC=$?

SUMMARY=$(cat "$SUMMARY_FILE")

# Claude отдаёт две сводки через строку-маркер: «что было» (три площадки) и
# «что делаем». Разрезаем здесь; маркера нет — значит вышла одна сводка, и она
# уходит первым сообщением, а второе просто не отправляется.
MARKER="---СВОДКА-2---"
SUMMARY_1=$(python3 -c "
import sys
text = open(sys.argv[1], encoding='utf-8').read()
print(text.split(sys.argv[2])[0].strip())
" "$SUMMARY_FILE" "$MARKER" 2>/dev/null)
SUMMARY_2=$(python3 -c "
import sys
text = open(sys.argv[1], encoding='utf-8').read()
parts = text.split(sys.argv[2])
print(parts[1].strip() if len(parts) > 1 else '')
" "$SUMMARY_FILE" "$MARKER" 2>/dev/null)
rm -f "$SUMMARY_FILE"

# Пуш результата в волт (ретраи и разбор конфликтов — в lib.sh).
#
# `projects/channel` целиком, а не списком путей как у утра: планёрка
# легитимно правит и рукописный контент-календарь (отмечает ✅ вышедшее,
# переносит невышедшие вставки), а его имя содержит период и меняется при
# смене календаря — перечислить его фиксированным путём нельзя. Плата за это
# известна: в автопуш попадёт всё, что окажется правленым в этой папке.
push_vault() {
  vault_push "content-planner: неделя $WEEK_START" "результат" "projects/channel"
}

if [ $RC -ne 0 ] || [ -z "$SUMMARY" ]; then
  log "ERROR: claude rc=$RC, сводка пустая — ничего не шлём"
  alert "Claude rc=$RC / пустая сводка — неделя $WEEK_START не собрана"
  push_vault
  exit 1
fi
if [ -z "$SUMMARY_2" ]; then
  log "WARN: маркера нет — сводка одна, шлю её и ссылку отдельно"
fi
if [ ! -f "$WEEKLY_FILE" ]; then
  log "WARN: недельный файл не создан — шлём только сводку"
fi

# --- сверка формата: разбирает ли неделю тот же парсер, что и утро
# Файл недели пишет модель, а вырезает из него день чистый парсер (dayblock.py),
# и формат заголовка дня для него — контракт, а не соглашение. Разъедется —
# dayblock вернёт 2, morning.sh напишет WARN и выйдет нулём, бот всю неделю
# будет слать «темы на сегодня в плане нет», и узнается это постфактум.
# Гоняем прямо здесь, все семь дней: в воскресенье в 18:00 есть время
# поправить файл руками, утром понедельника — уже нет.
# Каталог вырезки — временный: настоящие блоки дня в каталоге обмена трогать
# нельзя, их читает бот.
if [ -f "$WEEKLY_FILE" ]; then
  CHECK_DIR=$(mktemp -d)
  BAD=""
  WEEK_DATES=$(python3 -c "
import datetime, sys
start = datetime.date.fromisoformat(sys.argv[1])
for i in range(7):
    print(start + datetime.timedelta(days=i))
" "$WEEK_START" 2>>"$LOG")
  for d in $WEEK_DATES; do
    if ! python3 "$SCRIPT_DIR/dayblock.py" "$WEEKLY_FILE" "$d" "$CHECK_DIR" >/dev/null 2>>"$LOG"; then
      BAD="$BAD $d"
    fi
  done
  rm -rf "$CHECK_DIR"
  if [ -z "$WEEK_DATES" ]; then
    log "WARN: не посчитались даты недели для сверки формата — проверку пропустил"
  elif [ -n "$BAD" ]; then
    log "ERROR: dayblock не разбирает дни:$BAD"
    tg_notify "неделя $WEEK_START записана в формате, который не разбирает утренняя вырезка. Не разбираются дни:$BAD — по ним бот в 9:00 скажет «темы на сегодня в плане нет». Поправь заголовки («## Пн — YYYY-MM-DD — Рубрика») и поля («**ТГ:** …») руками сегодня"
  else
    log "dayblock: все семь дней недели $WEEK_START разбираются"
  fi
fi

# --- план недели: публикуем на Telegraph (форматированная страница, читается
# лучше плоского md на телефоне); ссылку добавляем к сводке. Сбой → файлом.
LINK=""
if [ -f "$WEEKLY_FILE" ]; then
  URL=$(python3 "$SCRIPT_DIR/telegraph.py" "$WEEKLY_FILE" 2>>"$LOG")
  if [ -n "$URL" ]; then
    log "telegraph: $URL"
    # URL нужен утренней доставке: tasks-bot цепляет ссылку на план недели
    # к утреннему сообщению, а сам Telegraph не вызывает. Файл кладём в
    # каталог обмена — тот же, откуда бот берёт блоки дня; в каталоге логов
    # он боту не виден.
    printf '%s\n' "$URL" > "$MORNING_DIR/telegraph-url-$WEEK_START.txt" \
      || log "WARN: не записал ссылку на план в $MORNING_DIR — утреннее сообщение уйдёт без неё"
    LINK=$'\n'"📅 План недели: $URL"
  else
    log "WARN: telegraph не удался — шлю файлом"
    tg_send_document "$WEEKLY_FILE" "Твоя неделя $WEEK_START – $WEEK_END"
  fi
fi

# Телеграм не принимает текст длиннее 4096 символов — обрезаем сводку (не ссылку)
# и помечаем многоточием, чтобы не потерять план недели в хвосте сообщения
TG_LIMIT=4096
send_summary() {   # $1 — текст сводки, $2 — хвост со ссылкой
  local body="$1" tail="$2" msg
  msg="$body$tail"
  if [ "${#msg}" -gt "$TG_LIMIT" ]; then
    body="${body:0:$(( TG_LIMIT - ${#tail} - 1 ))}…"
    msg="$body$tail"
    log "WARN: сводка обрезана до лимита телеграма"
  fi
  tg_send "$msg"
}

# Состояние отправки: если первое сообщение ушло, а второе упало, повторный
# прогон дошлёт только второе — «что было» не прилетит дублем.
# Живёт в данных скрипта, а не в волте: волт синкается в GitHub, и служебной
# отметке об отправке там не место.
SENT_FILE="$LOG_DIR/.sent-$WEEK_START"
touch "$SENT_FILE"

# Ссылка на план едет со вторым сообщением («что делаем») — там ей место по
# смыслу. Маркера в ответе не оказалось (сводка одна) — уйдёт отдельной строкой.
LINK_2="$LINK"

SEND_RC=200
if grep -q '^msg1$' "$SENT_FILE" 2>/dev/null; then
  log "сводка 1 уже отправлена раньше — пропускаю"
else
  SEND_RC=$(send_summary "$SUMMARY_1" "")
  log "sendMessage 1: HTTP $SEND_RC"
  case "$SEND_RC" in 2*) echo "msg1" >> "$SENT_FILE" ;; esac
fi

# DELIVERED — «всё, что должно было уйти, ушло». От него зависит, можно ли
# закрывать идеи в TickTick: закрыть недоставленную идею значит стереть её
# отовсюду, где Наташа могла бы её увидеть.
DELIVERED=0
SEND_RC_2=0
case "$SEND_RC" in
  2*)
    if [ -n "$SUMMARY_2" ]; then
      # Второе сообщение уходит только вслед за первым: «что делаем» без
      # «что было» читается как обрывок. Ссылка на план недели — в нём.
      SEND_RC_2=$(send_summary "$SUMMARY_2" "$LINK_2")
      log "sendMessage 2: HTTP $SEND_RC_2"
      case "$SEND_RC_2" in 2*) echo "msg2" >> "$SENT_FILE"; DELIVERED=1 ;; esac
    else
      # маркера в ответе не было — сводка одна, ссылку шлём отдельной строкой
      log "WARN: вторая сводка пустая (нет маркера) — шлю только ссылку"
      SEND_RC_2=200
      [ -n "$LINK_2" ] && SEND_RC_2=$(send_summary "$LINK_2" "")
      case "$SEND_RC_2" in 2*) echo "msg2" >> "$SENT_FILE"; DELIVERED=1 ;; esac
    fi
    ;;
esac

# --- закрыть в TickTick идеи, взятые в план — ТОЛЬКО после того как всё, что
# полагалось отправить, реально ушло (иначе идея пропадёт из TickTick, а Наташа
# её не увидит нигде)
case "$DELIVERED" in
  1)
    if [ -f "$TAKEN_FILE" ]; then
      # Разбор маркеров и ретраи — в ticktick.py, под тестами. Прошлый разбор
      # жил здесь на `grep -o 'id=[a-f0-9]*'` и молча ломался: «id=» находится
      # ещё и внутри «pid=», поэтому в id попадали ДВЕ строки, URL получался
      # битым, и curl возвращал HTTP 000. По логу — 66 попыток подряд, ни
      # одной удачной: идеи не закрывались никогда и возвращались в планёрку
      # каждую неделю. Починено 29.07.2026 (тест
      # test_id_внутри_слова_pid_не_путается_с_настоящим).
      if python3 "$SCRIPT_DIR/ticktick.py" complete --from-file "$TAKEN_FILE" >>"$LOG" 2>&1; then
        log "ticktick: взятые идеи закрыты"
        rm -f "$TAKEN_FILE"
      else
        # Файл НЕ удаляем: следующий прогон попробует снова, а идеи остаются
        # видимыми в TickTick — это безопасная сторона отказа.
        log "WARN: не все идеи закрылись — оставляю $TAKEN_FILE на следующий раз"
        tg_notify "не смогла закрыть часть идей в TickTick, они остались открытыми"
      fi
    fi
    ;;
  *)
    log "WARN: сводки дошли не полностью (1: HTTP $SEND_RC, 2: HTTP $SEND_RC_2) — идеи TickTick не закрываю"
    alert "сводка не отправилась (1: $SEND_RC, 2: $SEND_RC_2), идеи TickTick оставила открытыми"
    ;;
esac

# всё доставлено — состояние больше не нужно
if [ "$DELIVERED" = "1" ]; then rm -f "$SENT_FILE"; fi

push_vault
log "done"
