"""
AIMA Proposal Bot — daily approval workflow + batch review before sending.

Щоденний воркфлоу:
  08:00  Бот тегає @anatolii_malinovskyi у груповому чаті
  08-10  Маркетолог переглядає, виключає небажані контакти, відповідає "так" / /approve
  10:00  Якщо підтверджено → запускає розсилку, після завершення — мінімальний звіт
  10-14  Якщо ще не підтверджено → бот чекає; підтвердження в цей час → запуск одразу
  14:00  Якщо досі без підтвердження → розсилка переноситься на наступний робочий день

── Варіант 1: xlsx-обмін ──────────────────────────────────────────────
  /propose_v6    — надіслати xlsx-файл із контактами v6
  (відредагуйте xlsx: 1 у worker-do-not-send) → відправте файл назад

── Варіант 2: команда ─────────────────────────────────────────────────
  /exclude 121,124,130   — позначити pilot_id як worker-do-not-send=1

── Варіант 3: Google Таблиця ───────────────────────────────────────────
  /gdrive_v6     — посилання на Google Drive (доступ на редагування)

── Загальні ───────────────────────────────────────────────────────────
  /approve       — підтвердити розсилку
  /reject        — скасувати на сьогодні
  /status        — стан (батч + щоденний цикл)
  /list_excluded — показати виключених
  /start         — список команд

Run: python src/aima_proposal_bot.py
"""
import csv
import io
import json
import logging
import os
import re
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path

from typing import Dict, List, Optional, Tuple

import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
import requests

BASE_DIR = Path(__file__).parent.parent
DEFAULT_ENV_FILES = [
    BASE_DIR / ".env",
    Path(r"C:\python_scripts\top_1_telegram_signals\.env"),
]

PROPOSALS = {  # type: Dict[str, dict]
    "v6": {
        "name": "FAR v6 — 30 контактів (скачали, не зареєструвались)",
        "contacts_csv": BASE_DIR / "data/processed/aima_far_v6_contacts.csv",
        "log_csv":      BASE_DIR / "data/processed/aima_far_v6_telegram_log.csv",
        "preview_md":   BASE_DIR / "data/processed/aima_far_v6_gate1_preview.md",
        "send_script":  BASE_DIR / "src/aima_batch_send_v6.py",
        "gdrive_url":   "https://docs.google.com/spreadsheets/d/1DDrXnLZYWOQwuBgXupP2kODIz9jhg5xFMku67Ri5sM0/edit?usp=sharing",
    },
    "test": {
        "name": "TEST — 2 контакти (owner + AIMA)",
        "contacts_csv": BASE_DIR / "data/processed/aima_test_contacts.csv",
        "log_csv":      BASE_DIR / "data/processed/aima_test_telegram_log.csv",
        "send_script":  BASE_DIR / "src/aima_batch_send_test.py",
        "gdrive_url":   "",
    },
}

# Production defaults — overridden at startup from env (see main())
CHAT_ID           = -5178178519
MARKETER          = "@anatolii_malinovskyi"
ACTIVE_BATCH      = "v6"
MORNING_PING_HOUR = 8   # AIMA_PING_HOUR
SEND_HOUR         = 10  # AIMA_SEND_HOUR
DEADLINE_HOUR     = 14  # AIMA_DEADLINE_HOUR
# Sending is ONLY allowed Mon–Fri, 10:00–17:00 Kyiv. No exceptions — agreement with clients.
END_BUSINESS_HOUR = 17  # AIMA_END_BUSINESS_HOUR — hard wall: no send after this hour
ENV_LABEL         = ""  # "[🧪 local] " when AIMA_ENV=test/local

# Words that count as approval when used as a reply to the morning ping
APPROVAL_WORDS = {
    "так", "yes", "+", "ок", "ok", "добре", "гаразд",
    "підтверджую", "підтверджуємо", "відправляйте", "надсилайте", "approve",
}

STATE_FILE    = BASE_DIR / "data/processed/aima_bot_state.json"
VARIANTS_FILE = BASE_DIR / "data/processed/aima_pending_variants.json"
CLAUDE_BIN    = "claude"  # overridden in main() from CLAUDE_BIN env var
REVIEW_COLS = ["pilot_id", "first_name", "last_name", "phone",
               "registered_at", "variant", "worker-do-not-send"]

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)


# ── env ─────────────────────────────────────────────────────────────────

def load_env() -> None:
    for path in DEFAULT_ENV_FILES:
        if not path.exists():
            continue
        for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines():
            line = raw.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, v = line.split("=", 1)
            os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))


# ── state ────────────────────────────────────────────────────────────────

def _blank_daily(today: str) -> dict:
    return {
        "date": today,
        "status": "idle",        # idle | ping_sent | approved | sent | postponed | rejected
        "morning_msg_id": None,
        "send_triggered": False,
        "sent_count": None,
        "approved_by": None,
        "approved_at": None,
        "sent_at": None,
        "reminder_sent": False,
    }

def _merge_state_proposals(state: dict) -> None:
    """Merge proposals written by aima_agent_monitor into the global PROPOSALS dict."""
    for k, v in state.get("proposals", {}).items():
        if k not in PROPOSALS:
            PROPOSALS[k] = {
                "name":         v.get("name", k),
                "contacts_csv": Path(v["contacts_csv"]),
                "log_csv":      Path(v["log_csv"]),
                "send_script":  Path(v["send_script"]),
                "gdrive_url":   v.get("gdrive_url", ""),
            }


def load_state() -> dict:
    if STATE_FILE.exists():
        s = json.loads(STATE_FILE.read_text(encoding="utf-8"))
        _merge_state_proposals(s)
        return s
    return {"pending": None, "active_batch": ACTIVE_BATCH, "history": [], "daily": {}}

def save_state(s: dict) -> None:
    STATE_FILE.write_text(json.dumps(s, ensure_ascii=False, indent=2), encoding="utf-8")


# ── helpers ──────────────────────────────────────────────────────────────

def is_working_day(dt: datetime) -> bool:
    return dt.weekday() < 5  # Mon=0 … Fri=4


def read_contacts(key: str) -> Tuple[List[str], List[dict]]:
    path = PROPOSALS[key]["contacts_csv"]
    with path.open(encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        fields = list(reader.fieldnames or [])
        rows = [dict(r) for r in reader]
    return fields, rows

def write_contacts(key: str, fields: List[str], rows: List[dict]) -> None:
    path = PROPOSALS[key]["contacts_csv"]
    with path.open("w", encoding="utf-8", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)

def mark_excluded(key: str, pilot_ids: List[str]) -> Tuple[List[str], List[str]]:
    fields, rows = read_contacts(key)
    if "worker-do-not-send" not in fields:
        fields.insert(fields.index("phone") + 1, "worker-do-not-send")
    id_set = {str(p).strip() for p in pilot_ids}
    marked = []  # type: List[str]
    for row in rows:
        if row["pilot_id"] in id_set:
            row["worker-do-not-send"] = "1"
            marked.append(row["pilot_id"])
    not_found = [p for p in id_set if p not in marked]
    write_contacts(key, fields, rows)
    return marked, not_found

def sync_from_xlsx(key: str, xlsx_bytes: bytes) -> Tuple[int, int]:
    wb = openpyxl.load_workbook(io.BytesIO(xlsx_bytes))
    ws = wb.active
    headers = [str(c.value or "").strip() for c in next(ws.iter_rows(min_row=1, max_row=1))]
    try:
        pid_col = headers.index("pilot_id")
        dns_col = headers.index("worker-do-not-send")
    except ValueError:
        raise ValueError("xlsx не містить колонок pilot_id або worker-do-not-send")
    to_exclude = []
    for row in ws.iter_rows(min_row=2, values_only=True):
        pid = str(row[pid_col] or "").strip()
        dns = str(row[dns_col] or "").strip()
        if pid and dns in {"1", "true", "yes", "так"}:
            to_exclude.append(pid)
    if to_exclude:
        marked, _ = mark_excluded(key, to_exclude)
        return len(marked), ws.max_row - 1
    return 0, ws.max_row - 1

def build_xlsx(key: str) -> bytes:
    _, rows = read_contacts(key)
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = f"Розсилка {key}"
    hfill = PatternFill("solid", fgColor="1F4E79")
    dfill = PatternFill("solid", fgColor="FFF2CC")
    hfont = Font(color="FFFFFF", bold=True)
    ws.append(REVIEW_COLS)
    for cell in ws[1]:
        cell.font = hfont
        cell.fill = hfill
        cell.alignment = Alignment(horizontal="center")
    for row in rows:
        ws.append([row.get(c, "") for c in REVIEW_COLS])
    dns_idx = REVIEW_COLS.index("worker-do-not-send") + 1
    for cell in ws.iter_rows(min_row=2, min_col=dns_idx, max_col=dns_idx):
        for c in cell:
            c.fill = dfill
    widths = [10, 14, 16, 16, 14, 10, 22]
    for i, w in enumerate(widths, 1):
        ws.column_dimensions[openpyxl.utils.get_column_letter(i)].width = w
    ws.freeze_panes = "A2"
    buf = io.BytesIO()
    wb.save(buf)
    return buf.getvalue()

def excluded_summary(key: str) -> str:
    _, rows = read_contacts(key)
    excl = [r for r in rows if str(r.get("worker-do-not-send", "")).strip() == "1"]
    if not excl:
        return "Виключених немає."
    lines = [
        f"  • {r['pilot_id']} {r.get('first_name','')} {r.get('last_name','')} {r.get('phone','')}".rstrip()
        for r in excl
    ]
    return f"Виключено ({len(excl)}):\n" + "\n".join(lines)

def ready_count(key: str) -> int:
    _, rows = read_contacts(key)
    return sum(1 for r in rows if str(r.get("worker-do-not-send", "")).strip() != "1")


# ── bot ──────────────────────────────────────────────────────────────────

class Bot:
    def __init__(self, token: str) -> None:
        self.token = token
        self.base  = f"https://api.telegram.org/bot{token}"
        self.offset = 0
        self._lock  = threading.Lock()  # guards send_triggered flag

    # ── Telegram API wrappers ──────────────────────────────────────────

    def api(self, method: str, **kw) -> dict:
        return requests.post(f"{self.base}/{method}", timeout=30, **kw).json()

    def send(self, chat_id, text: str, **kw) -> dict:
        return self.api("sendMessage",
                        json={"chat_id": chat_id, "text": text,
                              "parse_mode": "HTML", **kw})

    def send_file(self, chat_id, data: bytes, filename: str, caption: str = "") -> dict:
        return self.api("sendDocument",
                        data={"chat_id": chat_id, "caption": caption},
                        files={"document": (filename, data)})

    def download(self, file_id: str) -> bytes:
        info  = self.api("getFile", json={"file_id": file_id})
        fpath = info["result"]["file_path"]
        return requests.get(
            f"https://api.telegram.org/file/bot{self.token}/{fpath}", timeout=30
        ).content

    def get_updates(self) -> List[dict]:
        r = self.api("getUpdates",
                     json={"offset": self.offset, "timeout": 20, "limit": 10})
        updates = r.get("result", [])
        if updates:
            self.offset = updates[-1]["update_id"] + 1
        return updates

    # ── incoming message handler ───────────────────────────────────────

    def handle(self, update: dict, state: dict) -> None:
        msg = update.get("message") or update.get("edited_message")
        if not msg:
            return

        chat_id  = msg["chat"]["id"]
        text     = (msg.get("text") or "").strip()
        document = msg.get("document")
        user     = msg.get("from", {})
        uname    = user.get("username") or user.get("first_name") or "?"

        # Incoming xlsx → sync exclusions
        if document:
            fname = document.get("file_name", "")
            if fname.endswith(".xlsx") and state.get("pending"):
                self._handle_xlsx(chat_id, document["file_id"], state, uname)
                return

        if not text:
            return

        # Check for free-text approval (reply to morning ping)
        if self._is_approval_msg(msg, state):
            self._handle_approval(chat_id, uname, state)
            return

        cmd  = text.split()[0].lstrip("/").lower().split("@")[0]
        args = text[len(cmd) + 1:].strip()

        log.info("[%s] %s: /%s %s", chat_id, uname, cmd, args[:60])

        active = state.get("active_batch", ACTIVE_BATCH)

        if cmd == "start":
            self._cmd_start(chat_id)
        elif cmd == "propose" or cmd.startswith("propose_"):
            key = cmd[len("propose_"):] if "_" in cmd else active
            self._cmd_propose(chat_id, key or active, state)
        elif cmd == "approve":
            self._handle_approval(chat_id, uname, state)
        elif cmd == "reject":
            self._cmd_reject(chat_id, uname, state)
        elif cmd == "exclude":
            self._cmd_exclude(chat_id, args, state)
        elif cmd == "gdrive" or cmd.startswith("gdrive_"):
            key = cmd[len("gdrive_"):] if "_" in cmd else active
            self._cmd_gdrive(chat_id, key or active, state)
        elif cmd == "list_excluded":
            key = state.get("pending") or ACTIVE_BATCH
            self.send(chat_id, excluded_summary(key))
        elif cmd == "status":
            self._cmd_status(chat_id, state)
        elif cmd == "myid":
            self._cmd_myid(chat_id, state)
        elif cmd == "pick":
            self._cmd_pick(chat_id, args, state)
        elif cmd == "more":
            self._cmd_more(chat_id)
        elif cmd == "chat":
            self._cmd_chat(chat_id, args)
        elif cmd == "push_approve":
            self._cmd_push_approve(chat_id, uname, state)

    def _is_approval_msg(self, msg: dict, state: dict) -> bool:
        """True if this message should be treated as a mailing approval."""
        text = (msg.get("text") or "").strip()
        if not text:
            return False
        # Must be a reply to the morning ping message
        morning_id = state.get("daily", {}).get("morning_msg_id")
        if not morning_id:
            return False
        reply_to = (msg.get("reply_to_message") or {})
        if reply_to.get("message_id") != morning_id:
            return False
        # Text must look like an approval
        t = text.lower().strip().rstrip("!.")
        return t in APPROVAL_WORDS or any(t.startswith(w) for w in APPROVAL_WORDS)

    # ── approval flow ──────────────────────────────────────────────────

    def _handle_approval(self, chat_id: int, uname: str, state: dict) -> None:
        """Record approval; trigger send immediately if it's already 10 AM+."""
        daily = state.setdefault("daily", {})
        status = daily.get("status", "idle")

        if status == "sent":
            self.send(chat_id, "ℹ️ Розсилку на сьогодні вже відправлено.")
            return
        if status == "postponed":
            self.send(chat_id, "ℹ️ Розсилку перенесено на наступний робочий день.")
            return
        if status == "approved":
            self.send(chat_id, "ℹ️ Розсилку вже підтверджено, очікую 10:00.")
            return

        key   = state.get("active_batch", ACTIVE_BATCH)
        n_ready = ready_count(key)
        now   = datetime.now()

        daily["status"]      = "approved"
        daily["approved_by"] = uname
        daily["approved_at"] = now.isoformat()
        save_state(state)

        if now.hour >= SEND_HOUR:
            if now.hour >= END_BUSINESS_HOUR or not is_working_day(now):
                self.send(chat_id,
                          f"⚠️ Підтверджено (@{uname}), але зараз поза робочими годинами.\n"
                          f"Розсилка дозволена лише у робочі дні з {SEND_HOUR:02d}:00 до {END_BUSINESS_HOUR:02d}:00.\n"
                          f"Статус збережено — розсилка запуститься наступного робочого дня о {SEND_HOUR:02d}:00.")
            else:
                self.send(chat_id,
                          f"✅ Підтверджено (@{uname}). Готово: <b>{n_ready}</b> контактів.\n"
                          f"Запускаю розсилку зараз...")
                self._trigger_send_async(state)
        else:
            self.send(chat_id,
                      f"✅ Підтверджено (@{uname}). Готово: <b>{n_ready}</b> контактів.\n"
                      f"Розсилка запуститься о <b>{SEND_HOUR:02d}:00</b>.")

    # ── batch send ─────────────────────────────────────────────────────

    def _trigger_send_async(self, state: dict) -> None:
        """Mark send_triggered and launch batch script in a background thread."""
        with self._lock:
            daily = state.setdefault("daily", {})
            if daily.get("send_triggered"):
                return
            daily["send_triggered"] = True
            save_state(state)

        key = state.get("active_batch", ACTIVE_BATCH)
        script = PROPOSALS[key]["send_script"]

        def _run() -> None:
            try:
                result = subprocess.run(
                    [sys.executable, str(script)],
                    cwd=str(BASE_DIR),
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    encoding="utf-8",
                    errors="replace",
                    timeout=3600,
                )
                # Parse "[done] sent=X no_account=Y errors=Z"
                sent = no_acc = errors = 0
                for line in result.stdout.splitlines():
                    m = re.search(r"\[done\].*sent=(\d+).*no_account=(\d+).*errors=(\d+)", line)
                    if m:
                        sent, no_acc, errors = int(m.group(1)), int(m.group(2)), int(m.group(3))
                        break

                s2 = load_state()
                s2.setdefault("daily", {}).update({
                    "status": "sent",
                    "sent_count": sent,
                    "sent_at": datetime.now().isoformat(),
                })
                save_state(s2)

                report = (
                    f"📬 <b>Розсилку {key} завершено</b>\n"
                    f"Надіслано: <b>{sent}</b> | Без акаунту: {no_acc} | Помилок: {errors}\n"
                    f"Час: {datetime.now().strftime('%H:%M')}"
                )
                if result.returncode not in (0, 255):
                    report += f"\n⚠️ Exit code: {result.returncode}"
                self.send(CHAT_ID, report)

            except subprocess.TimeoutExpired:
                self.send(CHAT_ID, "⚠️ Розсилка перевищила ліміт часу (60 хв).")
            except Exception as exc:
                log.error("Send subprocess error: %s", exc)
                self.send(CHAT_ID, f"❌ Помилка запуску розсилки: {exc}")

        threading.Thread(target=_run, daemon=True, name="batch-send").start()

    # ── daily scheduler ────────────────────────────────────────────────

    def _scheduler_loop(self) -> None:
        """Background thread: checks schedule every 30 s."""
        while True:
            time.sleep(30)
            try:
                self._check_schedule()
            except Exception as exc:
                log.error("Scheduler error: %s", exc)

    def _check_schedule(self) -> None:
        now   = datetime.now()
        today = now.strftime("%Y-%m-%d")

        if not is_working_day(now):
            return

        state = load_state()
        daily = state.setdefault("daily", {})

        # New working day starts at 08:00
        if daily.get("date") != today and now.hour >= 8:
            state["daily"] = _blank_daily(today)
            save_state(state)
            daily = state["daily"]

        if daily.get("date") != today:
            return  # Before 8 AM — nothing to do yet

        status = daily.get("status", "idle")

        # MORNING_PING_HOUR → send morning ping once
        if MORNING_PING_HOUR <= now.hour < SEND_HOUR and status == "idle":
            self._send_morning_ping(state)

        # SEND_HOUR+ → if approved and not yet triggered → run send
        elif now.hour >= SEND_HOUR and status == "approved" and not daily.get("send_triggered"):
            self._trigger_send_async(state)

        # One hour before deadline → send reminder if still waiting
        elif DEADLINE_HOUR - 1 <= now.hour < DEADLINE_HOUR and status == "ping_sent" and not daily.get("reminder_sent"):
            self._send_reminder(state)

        # DEADLINE_HOUR+ → if still waiting → postpone
        elif now.hour >= DEADLINE_HOUR and status in ("idle", "ping_sent"):
            self._postpone(state)

    def _send_reminder(self, state: dict) -> None:
        state["daily"]["reminder_sent"] = True
        save_state(state)
        key = state.get("active_batch", ACTIVE_BATCH)
        self.send(CHAT_ID,
                  f"⚠️ {ENV_LABEL}{MARKETER}, нагадування!\n\n"
                  f"Розсилка <b>{key}</b> ще не підтверджена.\n"
                  f"Якщо не підтвердити до <b>{DEADLINE_HOUR:02d}:00</b> — буде перенесено.\n\n"
                  f"Підтвердіть: /approve або відповідайте «так» на ранкове повідомлення.")
        log.info("Reminder sent (1h before deadline at %s:00)", DEADLINE_HOUR - 1)

    def _send_morning_ping(self, state: dict) -> None:
        key   = state.get("active_batch", ACTIVE_BATCH)
        p     = PROPOSALS[key]
        _, rows = read_contacts(key)
        total   = len(rows)
        excl    = sum(1 for r in rows if str(r.get("worker-do-not-send", "")).strip() == "1")
        n_ready = total - excl

        text = (
            f"🔔 {ENV_LABEL}{MARKETER}, доброго ранку!\n\n"
            f"Сьогодні о <b>{SEND_HOUR:02d}:00</b> запланована розсилка <b>{p['name']}</b>\n"
            f"Готово до відправки: <b>{n_ready}</b> | Виключено: {excl}\n\n"
            f"Якщо потрібно виключити контакт:\n"
            f"  • /exclude 121,124 — за pilot_id\n"
            f"  • /gdrive_{key} — переглянути таблицю\n\n"
            f"Щоб підтвердити — <b>відповідайте на це повідомлення</b> словом «так» або /approve\n\n"
            f"⏰ Без підтвердження до {SEND_HOUR:02d}:00 — розсилка чекатиме.\n"
            f"⏰ Без підтвердження до {DEADLINE_HOUR:02d}:00 — перенесеться на наступний робочий день."
        )
        result = self.send(CHAT_ID, text)
        msg_id = (result.get("result") or {}).get("message_id")

        state["daily"]["status"]         = "ping_sent"
        state["daily"]["morning_msg_id"] = msg_id
        state["pending"] = key
        save_state(state)
        log.info("Morning ping sent (msg_id=%s)", msg_id)

    def _postpone(self, state: dict) -> None:
        key = state.get("active_batch", ACTIVE_BATCH)
        state["daily"]["status"] = "postponed"
        save_state(state)
        self.send(CHAT_ID,
                  f"⏭ Розсилку <b>{key}</b> перенесено на наступний робочий день.\n"
                  f"Підтвердження не надійшло до {DEADLINE_HOUR:02d}:00.")
        owner_chat_id = state.get("owner_chat_id")
        if owner_chat_id:
            self.send(owner_chat_id,
                      f"⏭ {ENV_LABEL}Розсилку <b>{key}</b> перенесено — "
                      f"Анатолій не підтвердив до {DEADLINE_HOUR:02d}:00.\n"
                      f"Якщо узгодив вручну — /push_approve")
        log.info("Mailing postponed (no approval by %s:00)", DEADLINE_HOUR)

    # ── command handlers ───────────────────────────────────────────────

    def _cmd_start(self, chat_id) -> None:
        self.send(chat_id, (
            "📋 <b>AIMA Proposal Bot</b>\n\n"
            "<b>── Щоденний цикл ──</b>\n"
            "08:00 — бот тегає @anatolii_malinovskyi\n"
            "~08-10 — огляд, виключення, підтвердження\n"
            "10:00 — розсилка (якщо підтверджено)\n"
            "13:00 — нагадування (якщо ще немає підтвердження)\n"
            "14:00 — перенесення (якщо без підтвердження)\n\n"
            "<b>── Варіант 1: xlsx-обмін ──</b>\n"
            "1. /propose_v6 — отримати xlsx з контактами\n"
            "2. Поставте <code>1</code> у <b>worker-do-not-send</b>\n"
            "3. Відправте xlsx назад у чат\n\n"
            "<b>── Варіант 2: через команду ──</b>\n"
            "/exclude 121,124,130 — виключити за pilot_id\n\n"
            "<b>── Варіант 3: Google Таблиця ──</b>\n"
            "/gdrive_v6 — посилання на таблицю (редагування)\n\n"
            "<b>── Загальні ──</b>\n"
            "/approve — підтвердити розсилку\n"
            "/push_approve — примусово запустити розсилку зараз\n"
            "/reject — скасувати на сьогодні\n"
            "/list_excluded — показати виключених\n"
            "/status — поточний стан\n\n"
            "<b>── Агент ──</b>\n"
            "/pick AB — обрати варіанти A і B на завтра\n"
            "/more — аргументація: статистика, відповіді клієнтів, why\n"
            "/chat [інструкція] — поспілкуватись з агентом / перегенерувати гіпотези\n"
            "/myid — зареєструвати свій приват для сповіщень\n"
        ))

    def _cmd_propose(self, chat_id, key: str, state: dict) -> None:
        if key not in PROPOSALS:
            self.send(chat_id, f"❌ Батч '{key}' не знайдено.")
            return
        p = PROPOSALS[key]
        _, rows = read_contacts(key)
        total = len(rows)
        excl  = sum(1 for r in rows if str(r.get("worker-do-not-send", "")).strip() == "1")
        xlsx_data = build_xlsx(key)
        self.send_file(chat_id, xlsx_data, f"aima_{key}_review.xlsx",
                       caption=(f"📊 {p['name']}\n"
                                f"Всього: {total} | Виключено: {excl}\n\n"
                                f"Поставте 1 у worker-do-not-send і відправте файл назад, "
                                f"або /exclude 121,124"))
        state["pending"] = key
        save_state(state)

    def _handle_xlsx(self, chat_id, file_id: str, state: dict, uname: str) -> None:
        key = state["pending"]
        try:
            data = self.download(file_id)
            marked, total = sync_from_xlsx(key, data)
            self.send(chat_id, (
                f"✅ Файл оброблено.\n"
                f"worker-do-not-send: <b>{marked}</b> з {total}\n\n"
                f"{excluded_summary(key)}\n\n"
                f"Надішліть /approve або відповідайте «так» на ранкове повідомлення."
            ))
        except Exception as exc:
            self.send(chat_id, f"❌ Помилка читання xlsx: {exc}")

    def _cmd_exclude(self, chat_id, args: str, state: dict) -> None:
        key = state.get("pending") or ACTIVE_BATCH
        if not args:
            self.send(chat_id, "Використання: /exclude 121,124,130")
            return
        ids = [x.strip() for x in args.replace(";", ",").split(",") if x.strip()]
        if not ids:
            self.send(chat_id, "❌ Не вдалося розпізнати pilot_id.")
            return
        marked, not_found = mark_excluded(key, ids)
        lines = []
        if marked:
            lines.append(f"✅ Виключено: {', '.join(marked)}")
        if not_found:
            lines.append(f"⚠️ Не знайдено: {', '.join(not_found)}")
        lines.append(f"\n{excluded_summary(key)}")
        self.send(chat_id, "\n".join(lines))

    def _cmd_gdrive(self, chat_id, key: str, state: dict) -> None:
        if key not in PROPOSALS:
            self.send(chat_id, f"❌ Батч '{key}' не знайдено.")
            return
        p   = PROPOSALS[key]
        url = p.get("gdrive_url")
        if not url:
            # No GDrive URL (auto-generated batch) — send xlsx directly instead
            self._cmd_propose(chat_id, key, state)
            return
        _, rows = read_contacts(key)
        total = len(rows)
        excl  = sum(1 for r in rows if str(r.get("worker-do-not-send", "")).strip() == "1")
        state["pending"] = key
        save_state(state)
        self.send(chat_id, (
            f"📊 <b>{p['name']}</b>\n"
            f"Всього: {total} | Виключено: {excl}\n\n"
            f"<b>Таблиця з редагуванням:</b>\n{url}\n\n"
            f"Поставте <code>1</code> у <b>worker-do-not-send</b> для працівників.\n"
            f"Потім /approve або відповідайте «так» на ранкове повідомлення."
        ))

    def _cmd_myid(self, chat_id, state: dict) -> None:
        state["owner_chat_id"] = chat_id
        save_state(state)
        self.send(chat_id, "✅ Ваш chat_id <code>{}</code> збережено.\nВаріанти повідомлень надходитимуть сюди щодня о 15:15.".format(chat_id))

    def _cmd_pick(self, chat_id, args: str, state: dict) -> None:
        """Mark chosen variants in aima_pending_variants.json. Usage: /pick AB or /pick A C"""
        raw = args.upper().replace(",", "").replace(" ", "")
        labels = [ch for ch in raw if ch in "ABCD"]
        if len(labels) < 2:
            self.send(chat_id, "Використання: /pick AB або /pick A C\nОберіть два варіанти з A/B/C/D.")
            return
        if not VARIANTS_FILE.exists():
            self.send(chat_id, "❌ Файл варіантів не знайдено. Агент ще не запускався сьогодні.")
            return
        try:
            data = json.loads(VARIANTS_FILE.read_text(encoding="utf-8"))
            data["status"] = "chosen"
            data["chosen"] = labels[:2]
            data["picked_at"] = datetime.now().isoformat()
            VARIANTS_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        except Exception as exc:
            self.send(chat_id, "❌ Помилка оновлення варіантів: {}".format(exc))
            return
        variants_map = {v["label"]: v for v in data.get("variants", [])}
        info = "\n".join(
            "  <b>{}:</b> {}".format(lb, (variants_map.get(lb) or {}).get("text", "?")[:160])
            for lb in labels[:2]
        )
        self.send(chat_id, "{}✅ Обрано варіанти <b>{}</b>:\n{}\n\nАгент-монітор підбере їх сьогодні о 16:30.".format(
            ENV_LABEL, " та ".join(labels[:2]), info))

    def _cmd_more(self, chat_id: int) -> None:
        if not VARIANTS_FILE.exists():
            self.send(chat_id, "ℹ️ Агент ще не генерував варіанти. Запуск о 15:15.")
            return
        try:
            data = json.loads(VARIANTS_FILE.read_text(encoding="utf-8"))
        except Exception as exc:
            self.send(chat_id, "❌ Помилка читання варіантів: {}".format(exc))
            return

        stats        = data.get("stats", {})
        reply_samples = data.get("reply_samples", {})
        variants     = data.get("variants", [])
        raw          = data.get("raw_claude_response", "")
        status       = data.get("status", "")
        gen_at       = data.get("generated_at", "")[:16].replace("T", " ")
        is_fallback  = status.startswith("fallback")

        lines = ["🔍 <b>Аргументація агента</b>"]
        if gen_at:
            lines.append("<i>Згенеровано: {}</i>".format(gen_at))
        lines.append("")

        if stats:
            lines.append("<b>📊 Статистика по гіпотезах:</b>")
            for hyp, s in sorted(stats.items()):
                sent    = s.get("sent", 0)
                replied = s.get("replied", 0)
                rate    = "{:.0f}%".format(replied / sent * 100) if sent else "—"
                lines.append("  <b>{}</b>: {} надіслано, {} відповіли (<b>{}</b>)".format(
                    hyp[:35], sent, replied, rate))
            lines.append("")

        if reply_samples:
            lines.append("<b>💬 Що відповідали клієнти:</b>")
            for hyp, texts in sorted(reply_samples.items()):
                for t in texts[:4]:
                    lines.append('  [{}] "<i>{}</i>"'.format(hyp[:25], t[:100]))
            lines.append("")

        if not is_fallback and raw and not raw.startswith("claude failed"):
            lines.append("<b>🤖 Повна аргументація агента:</b>")
            lines.append(raw[:2500] + ("\n… (скорочено)" if len(raw) > 2500 else ""))
        else:
            lines.append("<b>🤖 Why для кожного варіанту:</b>")
            for v in variants:
                lines.append("<b>{}.</b> {}".format(v["label"], v["text"][:100]))
                if v.get("why"):
                    lines.append("   <i>Why: {}</i>".format(v["why"]))
                lines.append("")
            if is_fallback:
                lines.append("⚠️ <i>Агент не зміг згенерувати нові варіанти — використано попередні.</i>")

        msg = "\n".join(lines)
        if len(msg) > 4000:
            self.send(chat_id, msg[:4000])
            self.send(chat_id, msg[4000:8000])
        else:
            self.send(chat_id, msg)

    def _cmd_chat(self, chat_id: int, args: str) -> None:
        if not args:
            self.send(chat_id, (
                "💬 <b>/chat — розмова з агентом</b>\n\n"
                "Приклади:\n"
                "  /chat Перегенеруй гіпотези з акцентом на терміновість\n"
                "  /chat Зроби варіанти коротшими, до 100 символів\n"
                "  /chat Чому FAR_A показала кращі результати?\n"
                "  /chat Додай гіпотезу з соціальним доказом\n\n"
                "Якщо агент поверне варіанти A/B/C/D — вони автоматично збережуться."
            ))
            return
        self.send(chat_id, "⏳ Агент думає…")
        threading.Thread(
            target=self._run_chat_async,
            args=(chat_id, args),
            daemon=True,
            name="agent-chat",
        ).start()

    def _run_chat_async(self, chat_id: int, user_msg: str) -> None:
        stats_lines = []    # type: List[str]
        reply_lines = []    # type: List[str]
        variants_text = ""
        existing_data = {}  # type: dict

        if VARIANTS_FILE.exists():
            try:
                existing_data = json.loads(VARIANTS_FILE.read_text(encoding="utf-8"))
                for hyp, s in sorted(existing_data.get("stats", {}).items()):
                    sent = s.get("sent", 0)
                    replied = s.get("replied", 0)
                    rate = "{:.0f}%".format(replied / sent * 100) if sent else "—"
                    stats_lines.append("  {}: sent={} replied={} rate={}".format(hyp, sent, replied, rate))
                for hyp, texts in sorted(existing_data.get("reply_samples", {}).items()):
                    for t in texts[:3]:
                        reply_lines.append('  [{}] "{}"'.format(hyp[:30], t[:100]))
                for v in existing_data.get("variants", []):
                    variants_text += "{}. {}\n   Why: {}\n".format(
                        v["label"], v["text"], v.get("why", ""))
            except Exception:
                pass

        prompt = (
            "Ти маркетолог для AIMA — платформи для інтернет-магазинів в Україні.\n\n"
            "Поточна статистика відповідей:\n{stats}\n\n"
            "Реальні відповіді клієнтів:\n{replies}\n\n"
            "Поточні запропоновані варіанти:\n{variants}\n\n"
            "Цільова аудиторія: люди, що завантажили AIMA, але не створили магазин. "
            "B2C SaaS, Україна 2025-2026.\n\n"
            "Інструкція: {instruction}\n\n"
            "Якщо інструкція вимагає нових гіпотез — надай 4 варіанти СУВОРО у форматі "
            "(без зірочок, заголовків, блоків):\n"
            "A. [текст до 160 символів, від імені Насті]\n"
            "Why: [обґрунтування з best practices]\n\n"
            "B. [текст]\nWhy: [обґрунтування]\n\n"
            "C. [текст]\nWhy: [обґрунтування]\n\n"
            "D. [текст]\nWhy: [обґрунтування]\n\n"
            "Якщо це аналітичне запитання — відповідай без формату варіантів."
        ).format(
            stats="\n".join(stats_lines) or "Даних ще немає.",
            replies="\n".join(reply_lines) or "Немає.",
            variants=variants_text or "Немає.",
            instruction=user_msg,
        )

        try:
            result = subprocess.run(
                [CLAUDE_BIN, "--print"],
                input=prompt,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                encoding="utf-8",
                errors="replace",
                timeout=600,
                cwd=str(BASE_DIR),
            )
            if result.returncode != 0:
                self.send(chat_id, "❌ Агент не відповів (exit {}): {}".format(
                    result.returncode, result.stderr[:300]))
                return
            response = result.stdout.strip()
        except subprocess.TimeoutExpired:
            self.send(chat_id, "⏰ Агент не відповів за 10 хвилин.")
            return
        except Exception as exc:
            self.send(chat_id, "❌ Помилка виклику агента: {}".format(exc))
            return

        # Detect if response contains A/B/C/D variants
        variant_hits = re.findall(r'(?:^|\n)\s*[A-D][.\)]\s+\S', response)
        has_variants = len(variant_hits) >= 2

        if has_variants:
            variants = self._parse_variants(response)
        else:
            variants = []

        if len(variants) >= 2:
            # Save new variants
            existing_data.update({
                "generated_at": datetime.now().isoformat(),
                "status": "pending_choice",
                "chosen": [],
                "variants": variants,
                "raw_claude_response": response,
            })
            VARIANTS_FILE.write_text(
                json.dumps(existing_data, ensure_ascii=False, indent=2), encoding="utf-8")

            # Notify owner with new variants
            state = load_state()
            owner_chat_id = state.get("owner_chat_id")
            target = owner_chat_id or chat_id

            msg = (
                "\U0001f916 <b>{}Нові варіанти від агента</b>\n"
                "<i>Запит: {}</i>\n\n"
            ).format(ENV_LABEL, user_msg[:120])
            for v in variants:
                msg += "<b>{}.</b> {}\n".format(v["label"], v["text"])
                if v.get("why"):
                    msg += "   <i>Why: {}</i>\n".format(v["why"])
                msg += "\n"
            msg += "Оберіть два варіанти: /pick AB, /pick AC тощо\nДо 16:30 — інакше оберу сам."

            self.send(target, msg)
            if target != chat_id:
                self.send(chat_id, "✅ Агент перегенерував {} варіантів — надіслав тобі в приват.".format(len(variants)))
        else:
            # Analytical answer — just show it
            text = "🤖 <b>Агент відповідає:</b>\n\n" + response[:3800]
            if len(response) > 3800:
                self.send(chat_id, text)
                self.send(chat_id, response[3800:7000])
            else:
                self.send(chat_id, text)

    @staticmethod
    def _parse_variants(text):
        # type: (str) -> List[dict]
        variants = []  # type: List[dict]
        blocks = re.split(r'\n(?=\s*(?:\*\*)?[A-D][.\)]\s)', text)
        if len(blocks) < 2:
            blocks = [b.strip() for b in text.split("\n\n") if b.strip()]
        for block in blocks:
            label_m = re.match(r'^\s*\*{0,2}([A-D])[.\):]\*{0,2}\s*', block)
            if not label_m:
                continue
            label = label_m.group(1)
            why_m = re.search(r'\*{0,2}Why:\*{0,2}\s*(.+?)(?:\n|$)', block, re.IGNORECASE)
            why = why_m.group(1).strip().strip("*").strip() if why_m else ""
            quote_m = re.search(r'>\s*(.+?)(?=\n\n|\n\*{0,2}Why:|\Z)', block, re.DOTALL | re.IGNORECASE)
            if quote_m:
                msg = re.sub(r'\s+', ' ', quote_m.group(1)).strip()
            else:
                msg_end = why_m.start() if why_m else len(block)
                msg = block[label_m.end():msg_end]
                msg = re.sub(r'^\*+[^\n]+\*{0,2}\n', '', msg).strip()
                msg = re.sub(r'^\*{2,}', '', msg).strip()
            if msg:
                variants.append({"label": label, "text": msg[:200], "why": why})
            if len(variants) == 4:
                break
        return variants

    def _cmd_reject(self, chat_id, uname: str, state: dict) -> None:
        daily  = state.setdefault("daily", {})
        status = daily.get("status", "idle")
        if status in ("sent", "postponed"):
            self.send(chat_id, f"ℹ️ Розсилку вже оброблено ({status}).")
            return
        daily["status"] = "rejected"
        state["pending"] = None
        save_state(state)
        self.send(chat_id, f"❌ Розсилку на сьогодні скасовано (@{uname}).")

    def _cmd_push_approve(self, chat_id: int, uname: str, state: dict) -> None:
        """Force-send the mailing immediately. Only allowed Mon–Fri, SEND_HOUR–END_BUSINESS_HOUR."""
        now = datetime.now()
        if not is_working_day(now):
            self.send(chat_id,
                      f"❌ /push_approve недоступний у вихідні.\n"
                      f"Розсилка лише у робочі дні з {SEND_HOUR:02d}:00 до {END_BUSINESS_HOUR:02d}:00.")
            return
        if now.hour < SEND_HOUR or now.hour >= END_BUSINESS_HOUR:
            self.send(chat_id,
                      f"❌ /push_approve недоступний зараз ({now.strftime('%H:%M')}).\n"
                      f"Дозволено лише з <b>{SEND_HOUR:02d}:00</b> до <b>{END_BUSINESS_HOUR:02d}:00</b> у робочі дні.\n"
                      f"Домовленість з клієнтами: розсилка тільки у робочий час.")
            return
        daily  = state.setdefault("daily", {})
        status = daily.get("status", "idle")
        if status == "sent":
            self.send(chat_id, "ℹ️ Розсилку вже відправлено сьогодні.")
            return
        key     = state.get("active_batch", ACTIVE_BATCH)
        n_ready = ready_count(key)
        daily["status"]      = "approved"
        daily["approved_by"] = uname
        daily["approved_at"] = now.isoformat()
        save_state(state)
        self.send(chat_id,
                  f"🚀 {ENV_LABEL}Ручний запуск розсилки (@{uname})\n"
                  f"Батч: <b>{key}</b> | Готово: <b>{n_ready}</b> контактів.\n"
                  f"Запускаю зараз…")
        self._trigger_send_async(state)

    def _cmd_status(self, chat_id, state: dict) -> None:
        daily  = state.get("daily", {})
        ddate  = daily.get("date", "—")
        dstatus = daily.get("status", "idle")
        key    = state.get("active_batch", ACTIVE_BATCH)
        n_ready = ready_count(key)
        excl_text = excluded_summary(key)

        status_labels = {
            "idle":       "очікує ранку",
            "ping_sent":  "ранковий пінг надіслано, чекаю підтвердження",
            "approved":   "підтверджено, чекаю 10:00",
            "sent":       f"розсилку відправлено ({daily.get('sent_count', '?')} повідомлень)",
            "postponed":  "перенесено на наступний робочий день",
            "rejected":   "скасовано на сьогодні",
        }
        self.send(chat_id, (
            f"<b>Активний батч:</b> {key} | Готово до відправки: {n_ready}\n"
            f"<b>Дата:</b> {ddate} | <b>Стан:</b> {status_labels.get(dstatus, dstatus)}\n\n"
            f"{excl_text}"
        ))

    # ── main loop ──────────────────────────────────────────────────────

    def run(self) -> None:
        if os.environ.get("AIMA_DISABLE_SCHEDULER", "").lower() not in ("1", "true", "yes"):
            threading.Thread(
                target=self._scheduler_loop, daemon=True, name="scheduler"
            ).start()
            log.info("Bot started (scheduler running). Polling for updates...")
        else:
            log.info("Bot started (scheduler DISABLED — local/test mode). Polling for updates...")
        while True:
            try:
                state = load_state()
                for upd in self.get_updates():
                    self.handle(upd, state)
            except KeyboardInterrupt:
                log.info("Stopped by user.")
                break
            except Exception as exc:
                log.error("Loop error: %s", exc)
                time.sleep(5)


def main() -> None:
    load_env()
    global CHAT_ID, MARKETER, ACTIVE_BATCH, MORNING_PING_HOUR, SEND_HOUR, DEADLINE_HOUR, END_BUSINESS_HOUR, ENV_LABEL, CLAUDE_BIN
    CHAT_ID            = int(os.environ.get("AIMA_CHAT_ID",              str(CHAT_ID)))
    MARKETER           = os.environ.get("AIMA_MARKETER",                  MARKETER)
    ACTIVE_BATCH       = os.environ.get("AIMA_ACTIVE_BATCH",              ACTIVE_BATCH)
    MORNING_PING_HOUR  = int(os.environ.get("AIMA_PING_HOUR",             str(MORNING_PING_HOUR)))
    SEND_HOUR          = int(os.environ.get("AIMA_SEND_HOUR",             str(SEND_HOUR)))
    DEADLINE_HOUR      = int(os.environ.get("AIMA_DEADLINE_HOUR",         str(DEADLINE_HOUR)))
    END_BUSINESS_HOUR  = int(os.environ.get("AIMA_END_BUSINESS_HOUR",     str(END_BUSINESS_HOUR)))
    env_mode           = os.environ.get("AIMA_ENV", "").lower()
    ENV_LABEL          = "[🧪 local] " if env_mode in ("test", "local") else ""
    CLAUDE_BIN         = os.environ.get("CLAUDE_BIN", CLAUDE_BIN)
    token = os.environ.get("AIMA_BOT_TOKEN")
    if not token:
        raise SystemExit("AIMA_BOT_TOKEN missing from .env")
    Bot(token).run()


if __name__ == "__main__":
    main()
