"""Screen session lifecycle management.

All screen interactions go through subprocess calls to the `screen` binary.
"""

import os
import re
import shutil
import subprocess
import tempfile

from .config import DEFAULT_TAG, STATE_DETACHED
from .env_detect import DetectedEnv, EnvType, get_activate_command
from .naming import build_session_name
from .utils import log_info, log_success, log_warn


def screen_available() -> bool:
    """Check if GNU screen is installed."""
    return shutil.which("screen") is not None


# Matches smriti session names: <host>-<dir>-<hash>-<tag>-<YYYYMMDD-HHMMSS>
_SMRITI_NAME_RE = re.compile(r".+-\w{4}-\w+-\d{8}-\d{6}$")


def list_sessions(pattern: str | None = None) -> list[dict]:
    """List smriti screen sessions, optionally filtered by name pattern.

    Only returns sessions created by smriti (matching the naming convention).
    Returns list of dicts with keys:
        pid, name, state, timestamp (str or None), cwd (str or None)
    """
    result = subprocess.run(
        ["screen", "-ls"],
        capture_output=True,
        text=True,
    )

    sessions = []
    # screen -ls output format:
    #   12345.name   (02/12/2026 03:08:39 PM)    (Detached)
    for line in result.stdout.splitlines():
        line = line.strip()
        match = re.match(
            r"(\d+)\.(\S+)\s+"
            r"(?:\((\d{2}/\d{2}/\d{4}\s+\d{2}:\d{2}:\d{2}\s+[AP]M)\)\s+)?"
            r"\((\w+)\)\s*$",
            line,
        )
        if match:
            pid, name, timestamp, state = match.groups()
            # Only include smriti sessions
            if not _SMRITI_NAME_RE.match(name):
                continue
            if pattern is None or name.startswith(pattern):
                cwd = _get_session_cwd(pid)
                sessions.append(
                    {
                        "pid": pid,
                        "name": name,
                        "state": state,
                        "timestamp": timestamp,
                        "cwd": cwd,
                    }
                )

    return sessions


def _get_session_cwd(pid: str) -> str | None:
    """Get the working directory of the shell inside a screen session."""
    try:
        children_path = f"/proc/{pid}/task/{pid}/children"
        with open(children_path) as f:
            children = f.read().strip().split()
        for cpid in children:
            cwd = os.readlink(f"/proc/{cpid}/cwd")
            return cwd
    except (OSError, IndexError):
        pass
    return None


def get_latest_session(pattern: str | None = None) -> str | None:
    """Get the most recent session matching pattern (by timestamp in name).

    Since timestamps are YYYYMMDD-HHMMSS, lexicographic sort = chronological sort.
    """
    sessions = list_sessions(pattern=pattern)
    if not sessions:
        return None
    # Sort by name (timestamp at end ensures correct ordering)
    sessions.sort(key=lambda s: s["name"], reverse=True)
    # Prefer detached sessions
    detached = [s for s in sessions if s["state"] == STATE_DETACHED]
    if detached:
        return detached[0]["name"]
    return sessions[0]["name"]


def _detect_effective_shell() -> str:
    """Detect the shell the user actually ends up in.

    Some users have SHELL=/bin/bash but their .bashrc execs into zsh.
    We check for this pattern to use the correct init strategy.
    """
    shell = os.environ.get("SHELL", "/bin/bash")
    shell_name = os.path.basename(shell)

    if shell_name != "bash":
        return shell_name

    # Check if .bashrc execs into zsh (common pattern)
    bashrc = os.path.expanduser("~/.bashrc")
    if os.path.exists(bashrc):
        try:
            with open(bashrc) as f:
                for line in f:
                    stripped = line.strip()
                    if "exec zsh" in stripped and not stripped.startswith("#"):
                        return "zsh"
        except OSError:
            pass

    return shell_name


def _write_bash_init(
    activate_cmds: list[str],
    extra_command: str | None = None,
) -> str:
    """Write a bash --rcfile that sources .bashrc then activates env."""
    lines = [
        "# Smriti session init — sources user rc then activates env",
        'if [ -f "$HOME/.bashrc" ]; then',
        '    source "$HOME/.bashrc"',
        "fi",
        "",
    ]
    for cmd in activate_cmds:
        lines.append(cmd)
    if extra_command:
        lines.append("")
        lines.append(extra_command)

    tmp = tempfile.NamedTemporaryFile(
        mode="w",
        prefix="smriti-rc-",
        suffix=".bashrc",
        delete=False,
    )
    tmp.write("\n".join(lines) + "\n")
    tmp.close()
    return tmp.name


def _write_zsh_init(
    activate_cmds: list[str],
    extra_command: str | None = None,
) -> str:
    """Write a zsh init via ZDOTDIR that sources .zshrc then activates env.

    Creates a temp directory with a .zshrc that sources the user's real
    .zshrc first, then runs activation commands via a precmd hook
    (so they fire after p10k/oh-my-zsh/conda init complete).
    """
    zdotdir = tempfile.mkdtemp(prefix="smriti-zsh-")

    lines = [
        "# Smriti session init — sources user rc then activates env",
        'if [ -f "$HOME/.zshrc" ]; then',
        '    source "$HOME/.zshrc"',
        "fi",
        "",
    ]

    # Activation runs in precmd hook (after p10k/oh-my-zsh/conda init).
    # Extra command (e.g. claude) runs via a zle-line-init widget so it
    # has full terminal access (unlike precmd which p10k intercepts).
    hook_body = []
    for cmd in activate_cmds:
        hook_body.append(f"    {cmd}")

    if extra_command:
        # After activation, register a one-shot zle widget that types
        # and executes the command when the line editor is ready.
        hook_body.extend(
            [
                "    __smriti_run() {",
                f"        BUFFER={extra_command!r}",
                "        zle accept-line",
                "        zle -D zle-line-init 2>/dev/null",
                "    }",
                "    zle -N zle-line-init __smriti_run",
            ]
        )

    hook_body.append("    add-zsh-hook -d precmd __smriti_activate")
    hook_body.append("    unfunction __smriti_activate")

    lines.extend(
        [
            "autoload -Uz add-zsh-hook",
            "__smriti_activate() {",
            *hook_body,
            "}",
            "add-zsh-hook precmd __smriti_activate",
        ]
    )

    zshrc_path = os.path.join(zdotdir, ".zshrc")
    with open(zshrc_path, "w") as f:
        f.write("\n".join(lines) + "\n")

    return zdotdir


def create_session(
    tag: str = "session",
    env: DetectedEnv | None = None,
    extra_command: str | None = None,
) -> None:
    """Create a new screen session.

    If env/commands are needed, creates a wrapper script that activates the
    env then exec's the user's shell. This preserves the user's ~/.screenrc
    and avoids timing issues with `stuff`.
    """
    session_name = build_session_name(tag)

    activate_cmd = get_activate_command(env) if env else None

    # Activation commands (env setup — run in hook for zsh)
    activate_cmds = []
    if activate_cmd:
        activate_cmds.append(activate_cmd)

    log_success(f"Creating session: {session_name}")
    if env:
        if env.env_type != EnvType.CONDA_YAML_MISSING:
            log_info(f"Activating: {env.path_or_name}")

    if activate_cmds or extra_command:
        shell_name = _detect_effective_shell()

        if shell_name == "zsh":
            init_path = _write_zsh_init(activate_cmds, extra_command)
            env_dict = os.environ.copy()
            env_dict["ZDOTDIR"] = init_path
            subprocess.run(["screen", "-S", session_name], env=env_dict)
        else:
            init_path = _write_bash_init(activate_cmds, extra_command)
            shell = os.environ.get("SHELL", "/bin/bash")
            subprocess.run(["screen", "-S", session_name, shell, "--rcfile", init_path])
    else:
        subprocess.run(["screen", "-S", session_name])


def reattach_session(session_name: str) -> None:
    """Reattach to an existing screen session."""
    log_info(f"Reconnecting: {session_name}")
    subprocess.run(["screen", "-r", session_name])


def kill_session(session_name: str) -> None:
    """Kill a specific screen session."""
    subprocess.run(["screen", "-S", session_name, "-X", "quit"])
    log_success(f"Killed: {session_name}")


def kill_latest(pattern: str | None = None) -> bool:
    """Kill the most recent session, optionally filtered by pattern.

    Returns True if a session was killed, False if none found.
    """
    latest = get_latest_session(pattern=pattern)
    if latest:
        kill_session(latest)
        return True
    return False


def auto_connect(
    pattern: str | None = None,
    create_tag: str = DEFAULT_TAG,
    env: DetectedEnv | None = None,
    extra_command: str | None = None,
    cc_only: bool = False,
) -> None:
    """Auto-connect to latest session matching pattern.

    If none found, create a new one. When cc_only=True, filters to CC sessions
    globally (used for claude-auto without dir scoping).
    """
    from .utils import get_latest_from, only_cc_sessions

    if cc_only:
        sessions = only_cc_sessions(list_sessions())
        latest_dict = get_latest_from(sessions)
        latest = latest_dict["name"] if latest_dict else None
    else:
        latest = get_latest_session(pattern=pattern)

    if latest:
        reattach_session(latest)
    else:
        log_warn("No existing session found. Creating new one...")
        create_session(tag=create_tag, env=env, extra_command=extra_command)
