"""
Write log messages to per-thread log files under ~/aionetiface/logs.

If that directory does not exist, all log calls are silently ignored.
"""

import atexit
import os
import re
import sys
import threading
import traceback
from .fstr import fstr
from ..install import get_aionetiface_install_root

LOGS_ROOT_PATH = os.path.join(get_aionetiface_install_root(), "logs")

# Optional human-readable tag for log filenames -- e.g. the test runner
# sets AIONETIFACE_LOG_TAG=test_auto_connect_reverse before invoking
# python -m unittest. The tag is sanitised to a safe filename slug and
# prepended to the log filename, so logs become greppable by test name
# instead of being identifiable only by the opaque (pid, tid) pair.
LOG_TAG_ENV = "AIONETIFACE_LOG_TAG"
LOG_TAG_SLUG_RE = re.compile(r"[^A-Za-z0-9._-]+")


def log_tag_slug():
    """Return the sanitised value of AIONETIFACE_LOG_TAG, or '' if unset."""
    raw = os.environ.get(LOG_TAG_ENV, "")
    if not raw:
        return ""
    slug = LOG_TAG_SLUG_RE.sub("_", raw).strip("_")
    return slug[:80]  # cap so we don't blow past path-length limits on Windows


log_fds = {}

# Per-process sequential thread numbering, used when a tag is set so
# log filenames are deterministic and reference-able by tag alone
# (aionetiface_<tag>_0.log = main thread, _1.log = first worker, etc).
# The map is OS-thread-id -> sequential int.  Main thread lands at 0
# on first log() call from startup; workers take 1, 2, ... in
# log-touch order, which lines up with creation order in practice.
log_tid_seq = {}
log_tid_seq_lock = threading.Lock()


def seq_for_tid(tid):
    """Return the sequential 0,1,2,... index for this OS thread id."""
    with log_tid_seq_lock:
        seq = log_tid_seq.get(tid)
        if seq is None:
            seq = len(log_tid_seq)
            log_tid_seq[tid] = seq
        return seq


def close_log_fds():
    """Close all open log file descriptors at interpreter shutdown."""
    for fd in list(log_fds.values()):
        try:
            os.close(fd)
        except OSError:
            pass
    log_fds.clear()


atexit.register(close_log_fds)


def open_log_fd(tid):
    """Open (or reuse) the per-thread log file and return its OS-level file descriptor."""
    if tid not in log_fds:
        tag = log_tag_slug()
        if tag:
            # Sequential per-process thread index when a tag is set --
            # the tag already discriminates this run from every other,
            # so PID adds nothing but noise.  Deterministic filenames
            # (aionetiface_<tag>_0.log, _1.log, ...) let orchestrators
            # and scripts hardcode paths instead of globbing by PID.
            name = "aionetiface_" + tag + "_" + str(seq_for_tid(tid)) + ".log"
        else:
            # No tag -> fall back to (pid, tid) so concurrent un-tagged
            # processes don't collide.  Same shape as before this
            # change so non-orchestrated runs are unaffected.
            name = "aionetiface_" + str(os.getpid()) + "_" + str(tid) + ".log"
        path = os.path.join(LOGS_ROOT_PATH, name)

        log_fds[tid] = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)

        # Announce the freshly-opened log path on stdout so orchestrators
        # capturing the subprocess stream (gate_sweep, matrix_full) can
        # auto-discover it without grepping ~/aionetiface/logs by PID or
        # mtime.  One line per (pid, tid) pair the first time it logs.
        #
        # Gated on AIONETIFACE_LOG_TAG being set: orchestrators always
        # set the tag (it's how they correlate this subprocess's log
        # back to its iteration), so they still get the announcement.
        # Library users who just `import aionetiface` don't set the
        # tag and therefore don't get the stdout line — without this
        # gate, every application built on aionetiface / warpgate /
        # sidewire / namebump would spam one [LOG-LOCATION] per thread
        # on stdout the first time anything called log(), purely as
        # a side-effect of ~/aionetiface/logs existing on disk.
        if tag:
            try:
                sys.stdout.write("[LOG-LOCATION] aionetiface_log=" + path + "\n")
                sys.stdout.flush()
            except (OSError, ValueError):
                pass

    return log_fds[tid]


def log(msg):
    """Write msg to the per-thread log file; silently no-ops if the logs directory is absent."""
    if not os.path.exists(LOGS_ROOT_PATH):
        return

    if not isinstance(msg, (str, bytes)):
        return

    tid = threading.get_ident()
    fd = open_log_fd(tid)

    if isinstance(msg, bytes):
        os.write(fd, msg + b"\n")
    else:
        os.write(fd, msg.encode("utf-8") + b"\n")


def log_exception():
    """Log the current exception's traceback to the per-thread log file."""
    exc = "".join(traceback.format_exception(*sys.exc_info()))
    log("EXCEPTION: " + exc.strip())


def log_p2p(msg, node_id):
    """Log a p2p message prefixed with the node_id tag."""
    log(fstr("p2p <{0}>: {1}", (node_id, msg)))
