#!/bin/bash
# Утренний прогон: секция сегодняшнего дня из недельного плана → файл обмена.
#
# Две фазы, и они неравноправны:
#   Фаза 1 (чистый python, dayblock.py) — вырезка дня. Секунды, без сети,
#           без Claude. Она обязана отрабатывать всегда: в 9:00 tasks-bot
#           читает файл обмена и шлёт сообщение.
#   Фаза 2 (headless Claude) — сверка темы дня со свежей фактурой. Может
#           упасть, зависнуть, выйти за таймаут, остаться без логина.
#
# Контракт между ними: что бы ни случилось во второй фазе, в файле обмена
# лежит валидный блок. Держится тремя вещами:
#   - Claude пишет не в файл обмена, а в скретч рядом. Живой файл, который в
#     9:00 читает бот, не бывает недописанным: скрипт переносит скретч на
#     место одним mv, и только после проверки.
#   - Скретч заодно единственный признак того, что вторая фаза правда
#     работала: без него rc=0 и валидный (потому что нетронутый) файл обмена
#     выглядели бы как удачный прогон.
#   - Вырезка всё равно копируется в бэкап и возвращается, если файл обмена
#     оказался испорчен — на случай, если модель полезет писать в него сама.
# Валидность: первая строка `date: сегодня` и строка площадки `ТГ:`.
set -u

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
set -a; source "$SCRIPT_DIR/.env"; set +a

VAULT="${VAULT_ROOT:-$HOME/obsidian-vault}"
LOG_DIR="${PLANNER_DATA_DIR:-$HOME/content-planner-data}"
# Каталог обмена с tasks-bot — подкаталог данных планёрки, тот же, что в
# planerka.sh: блоки дня и ссылка на план недели лежат рядом.
OUT_DIR="${MORNING_CONTENT_DIR:-$LOG_DIR/morning}"
LOG="$LOG_DIR/morning.log"
PROMPT_FILE="$SCRIPT_DIR/prompt-morning.md"
# Память показанных выводов: без неё пост, вышедший за ×1.5, попадал бы в
# блок каждое утро две недели подряд. Живёт не в волте (там ей не место) и
# не в каталоге логов, а в каталоге обмена — вместе со скретчем это ровно
# те два файла, которые Claude пишет, и весь каталог целиком открывается
# ему на запись через --add-dir. Логи и run.log остаются вне зоны записи.
SEEN_FILE="$OUT_DIR/seen-выводы.txt"
CLAUDE_BIN="${CLAUDE_BIN:-$(command -v claude)}"
# Юнит даёт прогону 1800 секунд целиком. Claude укладывается в 7 минут,
# остальное — вырезка, проба логина, TickTick и пуш волта. Раньше бюджет юнита
# был 600 и упирался в эту сумму впритык; до 9:00, когда бот читает файл
# обмена, запаса всё равно много.
CLAUDE_TIMEOUT="${MORNING_CLAUDE_TIMEOUT:-420}"
mkdir -p "$LOG_DIR" "$OUT_DIR"

# Общее с planerka.sh: log, tg_send, tg_notify, vault_pull, vault_push,
# ticktick_ideas. Переменные выше — их вход, поэтому source идёт после них.
#
# Утро не «падает»: сообщение в 9:00 уедет в любом случае — поэтому все его
# сигналы предупреждающие, а не аварийные, и почти все с ключом дедупа
# (см. tg_notify в lib.sh: прогон ежедневный, а причины у него живут долго).
NOTIFY_PREFIX="утренний прогон"
NOTIFY_DEDUP_KEY="push"   # ключ для алертов о неудачном пуше волта
source "$SCRIPT_DIR/lib.sh"

# timeout есть не везде (на Маке — только gtimeout из coreutils). Нет ни
# того ни другого — зовём Claude без таймаута: хуже, чем с ним, но лучше,
# чем не звать вовсе; на Pi ограничитель всё равно даст systemd.
TIMEOUT_BIN="$(command -v timeout || command -v gtimeout || true)"

vault_pull

TODAY=$(date +%Y-%m-%d)
# Понедельник текущей недели считает weekdates.py, а не `date`: у BSD date
# (Мак) нет ключа -d, и прежняя строка молча отдавала пустоту — прогон
# выходил нулём, не собрав ничего, и проверить утро на Маке было нельзя.
MONDAY=$(python3 "$SCRIPT_DIR/weekdates.py" --current "$TODAY")
if [ -z "$MONDAY" ]; then
  log "ERROR: не посчитался понедельник недели — фаза 1 невозможна"
  tg_notify "не посчиталась дата недели, сообщение в 9:00 уйдёт без темы дня" "no-monday"
  exit 0
fi
# Относительный путь — для промпта (Claude запускается в корне волта),
# абсолютный — для dayblock.py и для следа.
WEEKLY_REL="projects/channel/planner/{channel} {plan} неделя – $MONDAY.md"
WEEKLY="$VAULT/$WEEKLY_REL"
# Инста-тредсовый план субботнего прогона. Утро читает оба файла: телеграмный
# владеет полем ТГ, этот — Threads, IG и хуком. Файла может не быть (суббота
# не отработала или ещё не наступила) — вырезка дня переживёт это молча.
INSTA_REL="projects/channel/planner/{channel} {plan} инста и тредс неделя – $MONDAY.md"
INSTA="$VAULT/$INSTA_REL"
BLOCK_FILE="$OUT_DIR/morning-$TODAY.md"
# Скретч лежит рядом с блоком, в том же каталоге и на той же файловой
# системе: перенос на место обязан быть одним mv, а не копированием.
SCRATCH="$OUT_DIR/.morning-scratch-$TODAY.md"

# Жива ли субботняя фича вообще. Задача 4 (сама субботняя планёрка) реализуется
# позже этой задачи — до неё файла инста-плана не будет ни разу, ни в одну
# неделю. Если в этот переходный период слать алерт "no-insta" при каждом
# отсутствии файла, получится ежедневный алерт, на который нечего ответить
# (см. preflight.py про ту же ловушку с приростом) — Наташа быстро научится
# его не читать. Поэтому сигналим о пропаже инста-плана, только если система
# доказанно живая: хотя бы раз в каталоге планёрки уже лежал файл по этой
# маске. Ни одного — молчим (в лог WARN всё равно пишем). Появился первый —
# с этой недели пропажа уже настоящий повод для алерта.
# nullglob обязателен: без него непойманный паттерн вернулся бы как есть,
# литеральной строкой с фигурными скобками, и `-gt 0` соврал бы про наличие
# файла. Включаем и сразу выключаем — глобальный shopt трогать не хотим.
shopt -s nullglob
_insta_seen=("$VAULT/projects/channel/planner/"'{channel} {plan} инста и тредс неделя – '*.md)
shopt -u nullglob
INSTA_FEATURE_LIVE=0
[ "${#_insta_seen[@]}" -gt 0 ] && INSTA_FEATURE_LIVE=1
unset _insta_seen

# ------------------------------------------------------------------ фаза 1
# Три исхода, и они не равноправны.
#   - Нет обоих планов — это и есть самый вероятный отказ всей системы: бот
#     в 9:00 скажет «темы на сегодня в плане нет», и так будет каждое утро,
#     пока файлы не поправят руками. Блок не собираем вовсе.
#   - Не хватает только одного плана — половина контента всё равно жива
#     (ТГ или Threads/IG), терять её из-за упавшей соседней площадки нельзя.
#     dayblock.py спокойно переживает нехватку одного из файлов (Task 1),
#     поэтому здесь просто предупреждаем и всё равно собираем блок.
# Молча ни один из исходов не узнать — поэтому тот же notify с дедупом, что
# на «claude не найден» и «не залогинен». Ключи у всех разные: причины разные
# и чинятся по-разному.
PHASE1_OK=0
if [ ! -f "$WEEKLY" ] && [ ! -f "$INSTA" ]; then
  log "WARN: ни файла недели ($WEEKLY), ни инста-плана ($INSTA) — блок не собран, это не авария прогона"
  tg_notify "ни телеграмного плана недели $MONDAY, ни инста-плана нет — сообщение в 9:00 уйдёт без темы. Похоже, не отработали обе планёрки" "no-plans"
else
  if [ ! -f "$WEEKLY" ]; then
    log "WARN: файла недели (ТГ) ещё нет ($WEEKLY) — соберу блок только из инста-плана"
    tg_notify "файла недели $MONDAY (ТГ) нет — сообщение в 9:00 уйдёт без темы для телеграма. Похоже, воскресная планёрка не отработала" "no-week"
  elif [ ! -f "$INSTA" ]; then
    log "WARN: инста-плана ещё нет ($INSTA) — соберу блок только из телеграмного плана"
    if [ "$INSTA_FEATURE_LIVE" = "1" ]; then
      tg_notify "инста-плана недели $MONDAY нет — сообщение в 9:00 уйдёт без Threads и IG. Похоже, субботняя планёрка не отработала" "no-insta"
    fi
  fi
  if python3 "$SCRIPT_DIR/dayblock.py" "$WEEKLY" "$TODAY" "$OUT_DIR" "$INSTA" >> "$LOG" 2>&1; then
    log "ok: блок на $TODAY готов"
    PHASE1_OK=1
  else
    log "WARN: блок на $TODAY не собран (дня нет в плане или файл битый)"
    tg_notify "в плане недели $MONDAY не нашлось дня $TODAY — сообщение в 9:00 уйдёт без темы. Обычно это значит, что заголовок дня записан не в том формате (нужно «## Пн — $TODAY — Рубрика»)" "no-day"
  fi
fi

# файлы обмена старше недели не нужны
find "$OUT_DIR" -name 'morning-*.md' -mtime +7 -delete 2>/dev/null || true
# ссылка на план живёт всю свою неделю (пишется в вс, читается по вс включительно),
# поэтому её чистим с запасом — иначе можно снести ещё нужную
find "$OUT_DIR" -name 'telegraph-url-*.txt' -mtime +14 -delete 2>/dev/null || true
# бэкапы вырезки и скретчи от прошлых прогонов (остаются только после аварии)
find "$LOG_DIR" -name '.morning-backup-*' -mtime +2 -delete 2>/dev/null || true
find "$OUT_DIR" -name '.morning-scratch-*' -mtime +2 -delete 2>/dev/null || true

# ------------------------------------------------------------------ фаза 2
# Дальше — необязательная часть. Каждый ранний выход оставляет на месте
# вырезку фазы 1, и это штатный исход, а не авария.
if [ "$PHASE1_OK" != "1" ]; then
  log "фаза 2 пропущена: нечего обогащать"
  exit 0
fi
if [ ! -f "$PROMPT_FILE" ]; then
  log "WARN: нет промпта $PROMPT_FILE — фаза 2 пропущена"
  exit 0
fi
if [ -z "$CLAUDE_BIN" ] || [ ! -x "$CLAUDE_BIN" ]; then
  log "WARN: claude не найден ($CLAUDE_BIN) — фаза 2 пропущена"
  tg_notify "claude не найден на машине прогона, сообщение уйдёт без сверки" "claude-bin"
  exit 0
fi
if [ ! -d "$VAULT" ]; then
  log "WARN: волт недоступен ($VAULT) — фаза 2 пропущена"
  exit 0
fi

# --- предполётная проверка источников
# Утро читает архивы и отчёты ниши молча: не отработал ночной пуллер — оно
# спокойно сверит тему дня по вчерашним цифрам, и блок будет выглядеть
# нормальным. Утренняя цепочка плотная (07:00 архив → 07:05 meta → 07:15 ниша
# → 07:30 EN → 08:15 мы), и до сих пор она держалась на одном «должны успеть».
#
# Проверяем здесь, а не в начале прогона: если фаза 2 всё равно не пойдёт,
# ругаться на несвежие источники незачем — их никто не читает.
# `--no-growth`: прирост собирает только воскресная планёрка, к пятнице файлу
# штатно четыре дня, и без флага алерт приходил бы каждый день до конца недели.
PREFLIGHT=$(python3 "$SCRIPT_DIR/preflight.py" "$VAULT" "$TODAY" --no-growth 2>>"$LOG")
if [ -n "$PREFLIGHT" ]; then
  log "preflight: $(echo "$PREFLIGHT" | tr '\n' ';')"
  # Ключ дедупа общий на все проблемы разом: причина у них обычно одна
  # (ночная цепочка не отработала), и три отдельных сообщения об одном сбое
  # читаются как три сбоя.
  tg_notify "источники утром не в порядке:
$PREFLIGHT

Тему дня сверю по тому, что есть." "preflight"
else
  log "preflight: источники на месте"
fi

# Копия вырезки — единственная страховка контракта. Всё, что ниже, вправе
# испортить файл обмена; вернуть его обязаны мы.
BACKUP="$LOG_DIR/.morning-backup-$TODAY"
cp "$BLOCK_FILE" "$BACKUP" || { log "WARN: не сделал бэкап вырезки — фаза 2 отменена"; exit 0; }

# Бэкап инста-файла — у него защиты не было вообще, в отличие от двух других.
# Блок обмена прикрыт этим же бэкапом и guard_block() на EXIT/TERM/INT.
# Недельный (ТГ) план модель вообще не правит руками: след туда кладёт
# детерминированный питон-блок ниже (сам заменяет прошлый маркер того же дня
# при повторном прогоне) — ошибиться в разметке он не может, потому что не
# сочиняет текст, а собирает его по шаблону. Инста-файл — единственный, где
# правки в разметку вносит сама модель (см. промпт, раздел «Второй файл»):
# маркер не туда, задвоенный блок при повторном прогоне, случайно снесённое
# поле — и файл в таком виде уедет в волт через VAULT_PATHS как есть, а
# dayblock.py потом будет молча деградировать по нему всю неделю. Алерт
# no-insta это не ловит: он про отсутствие файла, а не про порчу существующего.
# Файла может не быть вовсе (переходный период или суббота ещё не отработала)
# — тогда бэкапить и проверять нечего, INSTA_BACKUP остаётся пустым.
INSTA_BACKUP=""
if [ -f "$INSTA" ]; then
  INSTA_BACKUP="$LOG_DIR/.morning-backup-insta-$TODAY"
  if ! cp "$INSTA" "$INSTA_BACKUP"; then
    log "WARN: не сделал бэкап инста-файла — фаза 2 идёт без защиты этого файла"
    INSTA_BACKUP=""
  fi
fi

block_valid() {   # $1 — что проверяем: файл обмена или скретч
  local f="$1"
  [ -s "$f" ] || return 1
  [ "$(head -n 1 "$f")" = "date: $TODAY" ] || return 1
  grep -q '^ТГ:' "$f" || return 1
  return 0
}

restore_block() {
  # через временный файл: tasks-bot может читать блок ровно сейчас
  cp "$BACKUP" "$OUT_DIR/.morning-restore.tmp" \
    && mv -f "$OUT_DIR/.morning-restore.tmp" "$BLOCK_FILE"
}

# Модель писать в файл обмена не должна, но каталог ей открыт целиком —
# страховка на случай, если полезет.
guard_block() {
  if [ -n "${BACKUP:-}" ] && [ -f "$BACKUP" ] && ! block_valid "$BLOCK_FILE"; then
    restore_block && log "WARN: файл обмена испорчен — вернул вырезку фазы 1"
  fi
}
trap guard_block EXIT
trap 'guard_block; exit 143' TERM
trap 'guard_block; exit 130' INT

# --- проба логина: headless Claude на Pi живёт на интерактивном логине и уже
# протухал. Планёрку это ломало дважды в неделю, утро будет ломать каждый день,
# поэтому спрашиваем заранее и говорим вслух — молчаливая деградация здесь
# страшнее сбоя: блок будет приходить, просто переставший быть свежим.
# Запускается из каталога скрипта, а не из волта: в волте лежит большой
# CLAUDE.md, и подтягивать его ради одного слова — платить за пробу впятеро.
PROBE=$(cd "$SCRIPT_DIR" && ${TIMEOUT_BIN:+$TIMEOUT_BIN 90} "$CLAUDE_BIN" --model claude-haiku-4-5-20251001 -p 'ответь одним словом: ok' --output-format text 2>&1)
PROBE_RC=$?
if [ "$PROBE_RC" -ne 0 ]; then
  log "WARN: проба Claude не прошла (rc=$PROBE_RC): $(echo "$PROBE" | head -n 2 | tr '\n' ' ')"
  if echo "$PROBE" | grep -qiE 'authenticat|invalid api key|oauth|/login|not logged in|401'; then
    log "ERROR: Claude не залогинен — предупреждаю в телеграм"
    tg_notify "Claude не залогинен на Pi, сходи ssh pi и claude /login — блок ушёл без сверки со свежим" "login"
  fi
  exit 0
fi

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

# --- собрать промпт (значения через argv: в путях есть фигурные скобки,
# пробелы и кириллица, подстановка через sed их не переживёт)
IDEAS_TMP=$(mktemp)
printf '%s\n' "$IDEAS" > "$IDEAS_TMP"
PROMPT=$(python3 - "$PROMPT_FILE" "$IDEAS_TMP" "$TODAY" "$WEEKLY_REL" "$BLOCK_FILE" "$SCRATCH" "$SEEN_FILE" "$INSTA_REL" <<'PYEOF'
import sys
tpl = open(sys.argv[1], encoding="utf-8").read()
ideas = open(sys.argv[2], encoding="utf-8").read().strip()
out = (tpl.replace("{{TODAY}}", sys.argv[3])
          .replace("{{WEEKLY_FILE}}", sys.argv[4])
          .replace("{{BLOCK_FILE}}", sys.argv[5])
          .replace("{{SCRATCH_FILE}}", sys.argv[6])
          .replace("{{SEEN_FILE}}", sys.argv[7])
          .replace("{{INSTA_FILE}}", sys.argv[8])
          .replace("{{IDEAS}}", ideas))
sys.stdout.write(out)
PYEOF
)
rm -f "$IDEAS_TMP"
if [ -z "$PROMPT" ]; then
  log "WARN: промпт не собрался — фаза 2 отменена"
  exit 0
fi

log "start: фаза 2, день $TODAY, неделя $MONDAY"

# Скретч от прошлого прогона убираем до вызова: наличие файла после — это
# признак «модель действительно писала», и он должен быть свежим.
rm -f "$SCRATCH"

# --add-dir обязателен: Claude запускается в корне волта, а скретч и память
# выводов лежат вне его. Без этого acceptEdits тихо не разрешает запись
# наружу — модель отвечает «готово», а файла нет (проверено). Открываем
# ровно каталог обмена: логи и служебное состояние прогона остаются вне.
# Набор инструментов расписан --tools, а не разрешениями: проверено живьём,
# что --allowedTools разрешает, но не сужает — с одним белым списком модель
# спокойно выполняла команды шелла, а запреты по именам закрывали только
# перечисленное (после запрета Bash она нашла Monitor, после — оставались
# Skill и навык run). --tools задаёт сам доступный набор: пять инструментов,
# и шелл до него не дотягивается ничем. Запреты оставлены сверху страховкой.
CLAUDE_OUT=$(mktemp)
(cd "$VAULT" && ${TIMEOUT_BIN:+$TIMEOUT_BIN $CLAUDE_TIMEOUT} "$CLAUDE_BIN" -p "$PROMPT" \
  --model claude-opus-5 \
  --permission-mode acceptEdits \
  --add-dir "$OUT_DIR" \
  --tools "Read,Glob,Grep,Write,Edit" \
  --disallowedTools "Bash,Monitor,BashOutput,KillShell,Task,Agent,WebFetch,WebSearch,NotebookEdit" \
  --output-format text) > "$CLAUDE_OUT" 2>>"$LOG"
RC=$?

PHASE2_OK=0
if [ "$RC" -eq 124 ]; then
  log "WARN: Claude не уложился в ${CLAUDE_TIMEOUT}s — уедет вырезка фазы 1"
elif [ "$RC" -ne 0 ]; then
  log "WARN: Claude rc=$RC — уедет вырезка фазы 1"
elif [ ! -f "$SCRATCH" ]; then
  # Ровно тот молчаливый отказ, на котором погорел --add-dir: rc нулевой,
  # файл обмена валиден (потому что нетронут) — и без этой проверки в лог
  # ушло бы «фаза 2 отработала».
  log "WARN: Claude вернул rc=0, но скретча нет — фаза 2 ничего не написала, уедет вырезка фазы 1"
elif ! block_valid "$SCRATCH"; then
  log "WARN: скретч невалиден (нет строки date: $TODAY или строки ТГ:) — уедет вырезка фазы 1"
elif mv -f "$SCRATCH" "$BLOCK_FILE"; then
  PHASE2_OK=1
  log "ok: фаза 2 отработала — $(head -n 3 "$CLAUDE_OUT" | tr '\n' ' ')"
else
  log "WARN: не смог перенести скретч на место — уедет вырезка фазы 1"
fi
rm -f "$CLAUDE_OUT" "$SCRATCH"

# Файл обмена модель трогать не должна, но каталог ей открыт — проверяем.
if ! block_valid "$BLOCK_FILE"; then
  restore_block && log "WARN: файл обмена оказался испорчен — вернул вырезку фазы 1"
  PHASE2_OK=0
fi

# --- инста-файл модель правит напрямую (см. промпт) — проверяем результат тем
# же способом, каким его на неделе будет читать сама утренняя вырезка:
# dayblock.parse_day() по каждому из семи дней недели. Не «файл непустой», а
# именно «дни по-прежнему разбираются» — задвоенный маркер или съехавший
# заголовок дня оставляют файл непустым и внешне нормальным, но dayblock на
# нём молча деградирует всю неделю. Проверяем безусловно, не только при
# PHASE2_OK=1: таймаут может убить Claude посреди Edit, и файл останется
# наполовину переписанным даже при rc≠0.
if [ -n "$INSTA_BACKUP" ]; then
  INSTA_CHECK=$(python3 - "$SCRIPT_DIR" "$INSTA" "$MONDAY" <<'PYEOF'
import sys
from datetime import date, timedelta

sys.path.insert(0, sys.argv[1])
import dayblock

path, monday_s = sys.argv[2], sys.argv[3]
monday = date.fromisoformat(monday_s)

try:
    text = open(path, encoding="utf-8").read()
except OSError as e:
    print("файл не читается: {}".format(e))
    sys.exit(1)

bad = [
    d.isoformat()
    for d in (monday + timedelta(days=i) for i in range(7))
    if dayblock.parse_day(text, d.isoformat()) is None
]
if bad:
    print("не разбираются дни: {}".format(", ".join(bad)))
    sys.exit(1)
print("ok")
PYEOF
  )
  INSTA_CHECK_RC=$?
  if [ "$INSTA_CHECK_RC" -ne 0 ]; then
    log "WARN: инста-файл после фазы 2 не разбирается ($INSTA_CHECK) — возвращаю бэкап"
    if cp "$INSTA_BACKUP" "$INSTA"; then
      log "ok: инста-файл восстановлен из бэкапа"
    else
      log "ERROR: не смог восстановить инста-файл из бэкапа — правь руками: $INSTA_BACKUP"
    fi
    tg_notify "утренняя правка испортила инста-план недели $MONDAY ($INSTA_CHECK) — вернула файл из бэкапа. Threads/IG за сегодня в сообщении могут остаться вчерашними" "insta-corrupt"
  else
    rm -f "$INSTA_BACKUP"
  fi
  INSTA_BACKUP=""
fi

# --- след в волте. Claude недельный план не правит намеренно (иначе утро
# начнёт переписывать то, на что Наташа уже настроилась), поэтому историю
# решений пишет скрипт. След обязан лежать НИЖЕ полей дня: dayblock читает
# первое вхождение поля, и след выше полей подменил бы завтрашнюю вырезку.
# Трогаем только поле `ТГ:` — оно единственное, чем владеет недельный план.
# Threads/IG/Хук принадлежат $INSTA, и его собственную историю (в том числе
# когда последний раз стоял хук) модель пишет туда сама, своим комментарием
# (см. промпт, «Второй файл»). Если продублировать те же поля сюда, в
# телеграмный файл, у площадок появится два следа вместо одного, и ровно
# это запрещает промпт двумя абзацами выше.
TRACE_DONE=0
if [ "$PHASE2_OK" = "1" ]; then
  TRACE=$(python3 - "$WEEKLY" "$TODAY" "$BACKUP" "$BLOCK_FILE" <<'PYEOF' 2>>"$LOG"
import os
import re
import sys
import tempfile

weekly, today, before_path, after_path = sys.argv[1:5]
MARKER = "<!-- morning:{} -->".format(today)
DASH = r"[—–-]"
DAY_RE = re.compile(r"^##\s+[А-Яа-я]{2}\s+" + DASH + r"\s+(\d{4}-\d{2}-\d{2})")
FIELD_RE = re.compile(r"^(ТГ|Threads|IG):\s*(.*)$")
HOOK_RE = re.compile(r"^\s*🎬 хук:\s*(.*)$")


def parse(path):
    fields, hook, flashes, head = {}, "", [], ""
    with open(path, encoding="utf-8") as fh:
        for line in fh.read().splitlines():
            if line.startswith("✍️"):
                head = line.strip()
                continue
            m = FIELD_RE.match(line)
            if m:
                fields[m.group(1)] = m.group(2).strip()
                continue
            m = HOOK_RE.match(line)
            if m:
                hook = m.group(1).strip()
                continue
            if line.startswith("⚡️"):
                flashes.append(line.strip())
    return head, fields, hook, flashes


old_head, old_fields, old_hook, _ = parse(before_path)
new_head, new_fields, new_hook, flashes = parse(after_path)

body = []
changed = [k for k in ("ТГ",)
           if new_fields.get(k, "") != old_fields.get(k, "")]
if new_head != old_head:
    body.append("Заголовок дня: {}".format(new_head))
for k in changed:
    body.append("**{}:** {}".format(k, new_fields.get(k, "") or "—"))
body.extend(flashes)

if body:
    body = ["*Утренний прогон {} поправил:*".format(today), ""] + body
else:
    body = ["*Утренний прогон {}: без изменений — блок ушёл, как собрала фаза 1.*".format(today)]

text = open(weekly, encoding="utf-8").read()
lines = text.split("\n")
start = None
for i, line in enumerate(lines):
    m = DAY_RE.match(line)
    if m and m.group(1) == today:
        start = i
        break
if start is None:
    print("след не записан: дня {} нет в плане".format(today), file=sys.stderr)
    sys.exit(1)

end = len(lines)
for j in range(start + 1, len(lines)):
    if lines[j].startswith("## "):
        end = j
        break

section = lines[start:end]
# прошлый след того же дня (повторный прогон) заменяем целиком
for i, line in enumerate(section):
    if line.strip() == MARKER:
        section = section[:i]
        break
while section and not section[-1].strip():
    section.pop()
section += ["", MARKER] + body

out = "\n".join(lines[:start] + section + [""] + lines[end:])
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(weekly) or ".", prefix=".weekly-", suffix=".tmp")
try:
    with os.fdopen(fd, "w", encoding="utf-8") as fh:
        fh.write(out)
    os.replace(tmp, weekly)
except Exception:
    try:
        os.unlink(tmp)
    except OSError:
        pass
    raise
print("след записан: {}".format("; ".join(body[-3:]) if body else "пусто"))
PYEOF
)
  if [ -n "$TRACE" ]; then
    TRACE_DONE=1
    log "$TRACE"
  else
    log "WARN: след в недельный файл не записан"
  fi
fi

# --- пуш волта. Пока идёт прогон, волт мог уехать вперёд: автосинк с Мака,
# правки с телефона, другой бот. Ежедневный коммит с Пи — заметная конфликтная
# поверхность, поэтому тянем и пробуем снова.
# Ровно те пути, которые утро правит легитимно. `git add projects/channel`
# целиком захватывал бы и рукописное — agents.md, контент-календарь, сетку
# рубрик, аудиторию и позиционирование, бэклог идей, tasks.md. Claude в фазе 2
# работает в корне волта с автоприёмом правок, и случайная правка канона
# уезжала бы на GitHub и на Мак под коммитом «утренний след», непросмотренной.
#
# Что чем правится:
#   - файл недели  — сам скрипт (след решений ниже полей дня, см. выше);
#   - снапшоты и архив — прогон tgstat-puller в 07:00. Он их и коммитит сам
#     (pull.sh), но если его push отвалился, они остаются в дереве, и без них
#     здесь дерево так и осталось бы грязным.
VAULT_PATHS=(
  "$WEEKLY_REL"
  "$INSTA_REL"
  "projects/channel/{channel} {source} снапшоты просмотров.jsonl"
  "projects/channel/{channel} {source} TGStat архив постов канала.json"
  "projects/channel/{channel} {source} TGStat архив постов канала.md"
)

# Ретраи, разбор конфликтов и алерты — в lib.sh (NOTIFY_DEDUP_KEY=push задан
# в шапке: причина неудачного пуша живёт долго, а прогон ежедневный).
if [ "$TRACE_DONE" = "1" ]; then
  vault_push "content-planner: утренний след $TODAY" "утренний след" "${VAULT_PATHS[@]}"
fi

# Обратная сторона узкого `git add`: всё, что прогон правит не по праву (а
# фаза 2 работает в корне волта с автоприёмом), остаётся в дереве незакоммиченным
# — и назавтра `git pull --rebase` откажется работать у всех, кто тянет волт.
# Раз мы сознательно такое не коммитим, то обязаны сказать вслух: иначе
# получится та же тихая поломка, только с другой стороны.
if [ "${PLANNER_GIT_SYNC:-0}" = "1" ]; then
  # --untracked-files=no: untracked-файлы (`??`) не мешают `git pull --rebase`,
  # алерт должен ловить только отслеживаемые правки, которые реально его уронят.
  DIRTY=$(git -C "$VAULT" status --porcelain --untracked-files=no 2>/dev/null | head -n 5)
  if [ -n "$DIRTY" ]; then
    log "WARN: в волте остались незакоммиченные правки: $(echo "$DIRTY" | tr '\n' '; ')"
    tg_notify "в волте на Пи остались незакоммиченные правки — их автоматика не коммитит намеренно, но пока они там, завтрашние git pull будут отваливаться. Файлы: $(echo "$DIRTY" | tr '\n' '; ')" "dirty"
  fi
fi

# файл обмена в порядке — страховка больше не нужна
rm -f "$BACKUP"
BACKUP=""
log "done: фаза 2 $( [ "$PHASE2_OK" = "1" ] && echo 'отработала' || echo 'не отработала, ушла вырезка фазы 1')"
exit 0
