#!/usr/bin/env python3
"""Minimal Slack Socket Mode bridge for Codex or Claude Code.

This template is intentionally small:
- Slack receives a DM or app mention.
- The bridge turns the Slack message into a plain prompt.
- Codex or Claude Code runs locally and returns the final answer to Slack.
"""

from __future__ import annotations

import json
import os
import re
import subprocess
import tempfile
import time
from pathlib import Path

from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler


ROOT = Path(__file__).resolve().parent
ENV_FILE = ROOT / ".env"
LOG_FILE = ROOT / "bridge.log"


def load_env(path: Path = ENV_FILE) -> None:
    if not path.exists():
        return
    for line in path.read_text().splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))


def log(message: str) -> None:
    stamp = time.strftime("%Y-%m-%d %H:%M:%S %z")
    with LOG_FILE.open("a", encoding="utf-8") as fh:
        fh.write(f"[{stamp}] {message}\n")


def strip_slack_mentions(text: str) -> str:
    return re.sub(r"\s+", " ", re.sub(r"<@[^>]+>", "", text)).strip()


def should_handle_dm(event: dict) -> bool:
    if event.get("bot_id") or event.get("subtype") in {"bot_message", "message_deleted"}:
        return False
    if event.get("type") != "message":
        return False
    if not (event.get("text") or "").strip():
        return False
    return event.get("channel_type") == "im" or str(event.get("channel", "")).startswith("D")


def build_prompt(user_text: str, user_id: str, channel_id: str, thread_ts: str | None) -> str:
    return f"""Slack에서 온 사용자 요청입니다.

사용자: {user_id}
채널: {channel_id}
스레드: {thread_ts or "없음"}

요청:
{strip_slack_mentions(user_text)}

응답은 Slack DM에 바로 보낼 최종 답변만 한국어로 간결하게 작성하세요.
필요한 로컬 작업은 직접 수행하되, 비밀 토큰이나 개인 정보를 노출하지 마세요.
"""


def strip_json_lines(raw: str) -> str:
    lines: list[str] = []
    for line in raw.splitlines():
        stripped = line.strip()
        if stripped.startswith("{") and stripped.endswith("}"):
            try:
                json.loads(stripped)
                continue
            except json.JSONDecodeError:
                pass
        lines.append(line)
    return "\n".join(lines).strip()


def run_codex(prompt: str, workdir: str, timeout: int) -> str:
    codex_bin = os.environ.get("CODEX_BIN", "codex")
    sandbox = os.environ.get("CODEX_SANDBOX", "workspace-write")
    model = os.environ.get("CODEX_MODEL", "").strip()

    with tempfile.NamedTemporaryFile(prefix="slack-codex-", suffix=".txt", delete=False) as out:
        output_path = out.name

    cmd = [
        codex_bin,
        "exec",
        "--skip-git-repo-check",
        "-C",
        workdir,
        "-c",
        'approval_policy="never"',
        "--sandbox",
        sandbox,
        "--output-last-message",
        output_path,
    ]
    if model:
        cmd.extend(["--model", model])
    cmd.append(prompt)

    try:
        completed = subprocess.run(cmd, cwd=workdir, text=True, capture_output=True, timeout=timeout)
        answer = Path(output_path).read_text(encoding="utf-8").strip() if Path(output_path).exists() else ""
        if completed.returncode != 0:
            log(f"codex failed: {completed.stderr[-1500:]}")
            return "Codex 실행 중 오류가 났습니다. bridge.log를 확인해 주세요."
        return answer or strip_json_lines(completed.stdout) or "완료했습니다."
    except subprocess.TimeoutExpired:
        log("codex timeout")
        return "작업 시간이 너무 오래 걸렸습니다. 요청을 조금 더 작게 나눠 주세요."
    finally:
        Path(output_path).unlink(missing_ok=True)


def run_claude(prompt: str, workdir: str, timeout: int) -> str:
    claude_bin = os.environ.get("CLAUDE_BIN", "claude")
    model = os.environ.get("CLAUDE_MODEL", "").strip()
    permission_mode = os.environ.get("CLAUDE_PERMISSION_MODE", "acceptEdits").strip()

    cmd = [claude_bin, "-p", "--output-format", "text", "--permission-mode", permission_mode]
    if model:
        cmd.extend(["--model", model])
    cmd.append(prompt)

    try:
        completed = subprocess.run(cmd, cwd=workdir, text=True, capture_output=True, timeout=timeout)
        if completed.returncode != 0:
            log(f"claude failed: {completed.stderr[-1500:]}")
            return "Claude Code 실행 중 오류가 났습니다. bridge.log를 확인해 주세요."
        return completed.stdout.strip() or "완료했습니다."
    except subprocess.TimeoutExpired:
        log("claude timeout")
        return "작업 시간이 너무 오래 걸렸습니다. 요청을 조금 더 작게 나눠 주세요."


def run_agent(prompt: str) -> str:
    backend = os.environ.get("AI_BACKEND", "codex").strip().lower()
    workdir = os.environ.get("AI_WORKDIR", str(Path.home()))
    timeout = int(os.environ.get("AI_TIMEOUT_SECONDS", "900"))

    if backend == "codex":
        return run_codex(prompt, workdir, timeout)
    if backend == "claude":
        return run_claude(prompt, workdir, timeout)
    return "AI_BACKEND는 codex 또는 claude 중 하나여야 합니다."


def main() -> None:
    load_env()
    bot_token = os.environ.get("SLACK_BOT_TOKEN", "").strip()
    app_token = os.environ.get("SLACK_APP_TOKEN", "").strip()
    if not bot_token or not app_token:
        raise SystemExit("SLACK_BOT_TOKEN and SLACK_APP_TOKEN are required in .env")

    app = App(token=bot_token)

    @app.event("message")
    def handle_message(event, say):  # type: ignore[no-untyped-def]
        if not should_handle_dm(event):
            return
        thread_ts = event.get("thread_ts") or event.get("ts")
        say(text="받았습니다. 처리하고 답장드릴게요.", thread_ts=thread_ts)
        prompt = build_prompt(
            user_text=event.get("text", ""),
            user_id=event.get("user", "unknown"),
            channel_id=event.get("channel", "unknown"),
            thread_ts=thread_ts,
        )
        answer = run_agent(prompt)
        say(text=answer[:39000], thread_ts=thread_ts)

    @app.event("app_mention")
    def handle_app_mention(event, say):  # type: ignore[no-untyped-def]
        if event.get("bot_id") or not (event.get("text") or "").strip():
            return
        thread_ts = event.get("thread_ts") or event.get("ts")
        prompt = build_prompt(
            user_text=event.get("text", ""),
            user_id=event.get("user", "unknown"),
            channel_id=event.get("channel", "unknown"),
            thread_ts=thread_ts,
        )
        answer = run_agent(prompt)
        say(text=answer[:39000], thread_ts=thread_ts)

    log("starting Slack Socket Mode bridge")
    SocketModeHandler(app, app_token).start()


if __name__ == "__main__":
    main()
