import json
import os
import re
import shutil
import signal
import sqlite3
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional

from .config import KAGE_DB_PATH, KAGE_GLOBAL_DIR, get_global_config
from .db import log_execution, start_execution, update_execution, get_execution_pid
from .parser import TaskDef
from .ai.chat import clean_ai_reply, get_thinking_tag


def _normalize_headless_args(cmd: list[str]) -> list[str]:
    """When running without a TTY, avoid interactive approval prompts for AI CLIs."""
    if not cmd:
        return cmd

    exe = Path(cmd[0]).name

    try:
        is_tty = sys.stdin.isatty()
    except Exception:
        is_tty = False
    if is_tty:
        return cmd

    if exe == "codex":
        try:
            exec_idx = cmd.index("exec")
        except ValueError:
            return cmd

        # Split into codex-global args and exec args
        head = cmd[:exec_idx]
        tail = cmd[exec_idx + 1 :]

        # Remove exec-scoped convenience that can prompt on non-TTY
        tail = [part for part in tail if part != "--full-auto"]

        def _drop_flag_with_value(parts: list[str], flag: str) -> list[str]:
            out: list[str] = []
            i = 0
            while i < len(parts):
                if parts[i] == flag:
                    i += 2
                    continue
                out.append(parts[i])
                i += 1
            return out

        # Ensure global non-interactive policy before subcommand.
        head = _drop_flag_with_value(head, "--ask-for-approval")
        head = _drop_flag_with_value(head, "--sandbox")
        head = head + ["--ask-for-approval", "never", "--sandbox", "workspace-write"]

        return head + ["exec"] + tail

    if exe == "claude":
        # Ensure --dangerously-skip-permissions is present for claude
        # We also prefer -p (print mode) for non-interactive execution
        new_cmd = list(cmd)
        if "-p" not in new_cmd and "--print" not in new_cmd:
            # Insert -p after 'claude' if not present
            new_cmd.insert(1, "-p")

        if "--dangerously-skip-permissions" not in new_cmd:
            new_cmd.insert(1, "--dangerously-skip-permissions")
        if "--allow-dangerously-skip-permissions" not in new_cmd:
            new_cmd.insert(1, "--allow-dangerously-skip-permissions")

        return new_cmd

    if exe == "gemini":
        # Ensure --prompt (or -p) is used for headless mode.
        if "--prompt" not in cmd and "-p" not in cmd:
            new_cmd = list(cmd)
            if len(new_cmd) > 1:
                new_cmd.insert(-1, "-p")
            else:
                new_cmd.append("-p")
            return new_cmd

    return cmd


def _get_memory_dir(project_dir: Path, task_name: str) -> Path:
    """タスクのメモリディレクトリパスを返す。ディレクトリがなければ作成する。"""
    safe_name = re.sub(r"[^a-zA-Z0-9_\-]", "_", task_name)
    memory_dir = project_dir / ".kage" / "memory" / safe_name
    memory_dir.mkdir(parents=True, exist_ok=True)
    return memory_dir


def _load_recent_memories(memory_dir: Path, max_entries: int = 5) -> str:
    """直近N日分のメモリファイルを読み込み、結合した文字列として返す。"""
    date_files = sorted(memory_dir.glob("*.json"), reverse=True)
    # task.json はメモリファイルではないので除外
    date_files = [f for f in date_files if f.name != "task.json"]
    date_files = date_files[:max_entries]
    date_files.reverse()  # 古い順に並べる

    if not date_files:
        return ""

    parts = []
    for f in date_files:
        try:
            content = f.read_text(encoding="utf-8")
            date_label = f.stem  # e.g. "2026-02-28"
            parts.append(f"--- {date_label} ---\n{content}")
        except Exception:
            continue
    return "\n\n".join(parts)


def _load_task_json(memory_dir: Path) -> dict:
    """task.json を読み込む。存在しなければ空dictを返す。"""
    task_json_path = memory_dir / "task.json"
    if task_json_path.exists():
        try:
            return json.loads(task_json_path.read_text(encoding="utf-8"))
        except Exception:
            return {}
    return {}


def _compute_prompt_hash(prompt: str) -> str:
    """プロンプト本文のSHA256ハッシュを返す。"""
    import hashlib
    return hashlib.sha256(prompt.strip().encode("utf-8")).hexdigest()


def _deactivate_task(task_file: Path):
    if not task_file.exists():
        return
    content = task_file.read_text(encoding="utf-8")
    new_content = re.sub(r"active:\s*(true|false)", "active: false", content)
    if content == new_content:
        # active が無い場合は cron: の後に追加
        new_content = re.sub(r"(cron:.*?\n)", r"\1active: false\n", content)
    task_file.write_text(new_content, encoding="utf-8")
    print(f"Task deactivated: {task_file.name}")


def _get_lock_path(project_dir: Path, task_name: str) -> Path:
    safe_proj = re.sub(r"[^a-zA-Z0-9_\-]", "_", project_dir.name)
    safe_task = re.sub(r"[^a-zA-Z0-9_\-]", "_", task_name)
    path = KAGE_GLOBAL_DIR / "locks" / f"{safe_proj}_{safe_task}.lock"
    path.parent.mkdir(parents=True, exist_ok=True)
    return path


def _check_running(lock_path: Path) -> Optional[int]:
    if not lock_path.exists():
        return None
    try:
        pid = int(lock_path.read_text().strip())
        os.kill(pid, 0)  # Check if process exists
        return pid
    except (ProcessLookupError, ValueError, OverflowError):
        return None

def _notify_connectors(task: TaskDef, status: str, stdout: str, stderr: str):
    if not task.notify_connectors:
        return
        
    from .connectors.runner import get_connector
    
    msg = f"**[{task.name}]** Execution completed with status: `{status}`"
    
    if stdout:
        msg += f"\n{stdout[:2000]}"
        if len(stdout) > 2000:
            msg += "\n...(truncated)"
        
    for c_name in task.notify_connectors:
        connector = get_connector(c_name)
        if connector:
            connector.send_message(msg)


def stop_execution(exec_id: str):
    """実行中のタスクを停止する。"""
    pid = get_execution_pid(exec_id)
    if not pid:
        print(f"No PID found for execution {exec_id}")
        return

    try:
        # プロセスグループ全体にSIGTERMを送信
        os.killpg(os.getpgid(pid), signal.SIGTERM)
        update_execution(exec_id, "STOPPED", "", "Terminated by user")
        print(f"Stopped execution {exec_id} (PID: {pid})")
    except ProcessLookupError:
        # 既に終了している場合
        update_execution(exec_id, "STOPPED", "", "Terminated by user (already dead)")
    except Exception as e:
        print(f"Failed to stop execution {exec_id}: {e}")


def execute_task(project_dir: Path, task: TaskDef, task_file: Optional[Path] = None):
    if not task.active:
        print(f"Skipping inactive task '{task.name}' in {project_dir}")
        return

    from .config import get_system_prompt
    from .parser import ConcurrencyPolicy, ExecutionMode

    # 多重起動チェック
    lock_path = _get_lock_path(project_dir, task.name)
    running_pid = _check_running(lock_path)

    if running_pid:
        if task.concurrency_policy == ConcurrencyPolicy.FORBID:
            print(
                f"Task '{task.name}' is already running (PID: {running_pid}). Skipping."
            )
            return
        elif task.concurrency_policy == ConcurrencyPolicy.REPLACE:
            print(
                f"Task '{task.name}' is already running (PID: {running_pid}). Terminating and replacing."
            )
            try:
                os.kill(running_pid, signal.SIGTERM)
            except Exception:
                pass

    global_config = get_global_config(workspace_dir=project_dir)
    system_prompt = get_system_prompt(workspace_dir=project_dir)

    # ロックファイル作成
    try:
        lock_path.write_text(str(os.getpid()))

        # メモリの読み込み（直近N件）
        memory_dir = _get_memory_dir(project_dir, task.name)
        memory_context_str = _load_recent_memories(
            memory_dir, max_entries=global_config.memory_max_entries
        )

        provider = None
        parser_type = "raw"
        p_args = ""
        cmd = []

        if task.prompt:
            # タスク管理 (task.json) の読み込みと prompt_hash チェック
            task_plan = _load_task_json(memory_dir)
            current_hash = _compute_prompt_hash(task.prompt)

            task_plan_context = ""
            if task_plan:
                stored_hash = task_plan.get("prompt_hash", "")
                plan_json = json.dumps(task_plan, indent=2, ensure_ascii=False)
                if stored_hash == current_hash:
                    task_plan_context = (
                        f"\n\n## Task Plan (.kage/memory/{re.sub(r'[^a-zA-Z0-9_-]', '_', task.name)}/task.json)\n"
                        f"{plan_json}"
                    )
                else:
                    task_plan_context = (
                        f"\n\n## Task Plan (.kage/memory/{re.sub(r'[^a-zA-Z0-9_-]', '_', task.name)}/task.json)\n"
                        f"\u26a0 **The task prompt has been updated** (hash mismatch). "
                        f"Review the previous task plan below and regenerate it based on the new instructions. "
                        f"Reuse completed work where applicable. Update `prompt_hash` to `{current_hash}`.\n\n"
                        f"Previous plan:\n{plan_json}"
                    )
            else:
                task_plan_context = (
                    f"\n\n## Task Plan\n"
                    f"No task plan found. Create one by writing to "
                    f"`.kage/memory/{re.sub(r'[^a-zA-Z0-9_-]', '_', task.name)}/task.json`.\n"
                    f"Use `prompt_hash`: `{current_hash}`"
                )

            # メモリコンテキスト
            if memory_context_str:
                memory_section = f"\n\n## Recent Memory (most recent {global_config.memory_max_entries} entries)\n{memory_context_str}"
            else:
                memory_section = (
                    f"\n\n## Recent Memory\n"
                    f"No previous memory found. You can write memory files to "
                    f"`.kage/memory/{re.sub(r'[^a-zA-Z0-9_-]', '_', task.name)}/YYYY-MM-DD.json`."
                )

            # プロバイダーの解決
            engine_name = task.provider or global_config.default_ai_engine
            if not engine_name:
                msg = "AIエンジンが未指定です。"
                print(f"[kage] ERROR: {msg}")
                log_execution(str(project_dir), task.name, "FAILED", "", msg)
                return

            # プロンプトの構築: System Prompt + Task Plan + Memory + Task Instructions
            thinking_tag = get_thinking_tag(engine_name)
            formatted_system_prompt = system_prompt.replace("{thinking_tag}", thinking_tag)
            full_prompt = f"{formatted_system_prompt}{task_plan_context}{memory_section}\n\n## Task Instructions\n{task.prompt}"

            provider = global_config.providers.get(engine_name)
            extra_args = task.ai.args if task.ai and task.ai.args else []

            resolved_template = None
            if task.command_template:
                resolved_template = task.command_template
            elif provider:
                cmd_def = global_config.commands.get(provider.command)
                if cmd_def:
                    resolved_template = cmd_def.template

            parser_type = task.parser or (provider.parser if provider else "raw")
            p_args = task.parser_args or (provider.parser_args if provider else "")

            if resolved_template:
                cmd = [
                    part.replace("{prompt}", full_prompt) for part in resolved_template
                ]
                cmd.extend(extra_args)
            else:
                cmd = [engine_name, full_prompt] + extra_args

        elif task.command:
            shell_cmd = task.shell or "sh"
            cmd = [shell_cmd, "-c", task.command]
        else:
            log_execution(
                str(project_dir),
                task.name,
                "FAILED",
                "",
                "No prompt or command specified",
            )
            return

        try:
            from .db import init_db
            init_db()  # Migration ensure
            # まだPIDがわからないのでNoneで開始
            exec_id = start_execution(str(project_dir), task.name)

            print(f"Executing task '{task.name}' in {project_dir}")
            env = os.environ.copy()
            if global_config.env_path:
                env["PATH"] = global_config.env_path

            if cmd and cmd[0]:
                exe_path = shutil.which(cmd[0], path=env.get("PATH"))
                if exe_path:
                    cmd[0] = exe_path
            cmd = _normalize_headless_args(cmd)

            # タイムアウト設定 (デフォルトなし)
            timeout = task.timeout_minutes * 60 if task.timeout_minutes else None

            # プロセス開始
            proc = subprocess.Popen(
                cmd,
                cwd=project_dir,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                env=env,
                start_new_session=True,  # 新しいセッション（プロセスグループ）を作成
            )

            # PID を更新
            conn = sqlite3.connect(KAGE_DB_PATH)
            cursor = conn.cursor()
            cursor.execute("UPDATE executions SET pid = ? WHERE id = ?", (proc.pid, exec_id))
            conn.commit()
            conn.close()

            try:
                stdout, stderr = proc.communicate(timeout=timeout)
                returncode = proc.returncode
            except subprocess.TimeoutExpired:
                # タイムアウト時はプロセスグループごと終了させる
                os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
                stdout, stderr = proc.communicate()
                returncode = -1
                raise  # re-raise to be caught by outer try

            result = subprocess.CompletedProcess(cmd, returncode, stdout, stderr)

            # autostop チェック: task.json の sub_tasks が全て done の場合
            if task.prompt and task.mode == ExecutionMode.AUTOSTOP and task_file:
                should_stop = False
                # task.json による判定
                updated_plan = _load_task_json(memory_dir)
                if updated_plan.get("sub_tasks"):
                    all_done = all(
                        t.get("status") == "done"
                        for t in updated_plan["sub_tasks"]
                    )
                    if all_done:
                        should_stop = True
                if should_stop:
                    _deactivate_task(task_file)

            if task.mode == ExecutionMode.ONCE and task_file:
                _deactivate_task(task_file)

            if task.prompt and parser_type == "jq" and p_args:
                try:
                    jq_result = subprocess.run(
                        ["jq", "-r", p_args],
                        input=result.stdout,
                        capture_output=True,
                        text=True,
                        env=env,
                    )
                    if jq_result.returncode == 0:
                        result.stdout = jq_result.stdout
                    else:
                        result.stderr += f"\n[jq err]: {jq_result.stderr}"
                except Exception as jq_e:
                    result.stderr += f"\n[jq exc]: {str(jq_e)}"

            status = "SUCCESS" if result.returncode == 0 else "FAILED"
            # Clean thinking tags from AI output before storing and notifying
            if task.prompt:
                result.stdout = clean_ai_reply(result.stdout)
            update_execution(exec_id, status, result.stdout, result.stderr)
            _notify_connectors(task, status, result.stdout, result.stderr)
        except subprocess.TimeoutExpired:
            stderr = f"Task timed out after {task.timeout_minutes} minutes"
            update_execution(
                exec_id,
                "TIMEOUT",
                "",
                stderr,
            )
            _notify_connectors(task, "TIMEOUT", "", stderr)
        except Exception as e:
            update_execution(exec_id, "ERROR", "", str(e))
            _notify_connectors(task, "ERROR", "", str(e))
    finally:
        if lock_path.exists():
            lock_path.unlink()
        
        # 最終的に PID を NULL にしておく (終了済みを明示)
        try:
            conn = sqlite3.connect(KAGE_DB_PATH)
            cursor = conn.cursor()
            cursor.execute("UPDATE executions SET pid = NULL WHERE id = ?", (exec_id,))
            conn.commit()
            conn.close()
        except:
            pass
