from __future__ import annotations

import os
import re
import threading
from datetime import datetime

from utils.path_safety import safe_resolve_path, secure_write_file

_SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9._-]+")
_SCRATCH_WRITE_LOCK = threading.RLock()


def _get_subagent_config() -> dict:
    try:
        from core.config import get_subagent_config

        return get_subagent_config().copy()
    except Exception:
        return {}


def sanitize_component(raw: str, fallback: str) -> str:
    text = (raw or "").strip()
    if not text:
        return fallback
    text = _SAFE_COMPONENT_RE.sub("_", text).strip("._-")
    while ".." in text:
        text = text.replace("..", "_")
    if not text:
        return fallback
    return text[:64]


def resolve_context(parent_thread_id: str, delegation_job_id: str) -> tuple[str, str]:
    parent = (parent_thread_id or "").strip()
    job = (delegation_job_id or "").strip()
    if not parent or not job:
        try:
            from subagent_runner import (
                get_current_delegation_job_id,
                get_current_delegation_parent_thread_id,
            )

            if not parent:
                parent = (get_current_delegation_parent_thread_id() or "").strip()
            if not job:
                job = (get_current_delegation_job_id() or "").strip()
        except Exception:
            pass
    safe_parent = sanitize_component(parent, "_headless")
    safe_job = sanitize_component(job, "")
    return safe_parent, safe_job


def resolve_delegation_scratch_path(
    parent_thread_id: str,
    delegation_job_id: str,
    *,
    allow_write: bool,
) -> tuple[str | None, str, str, str]:
    safe_parent, safe_job = resolve_context(parent_thread_id, delegation_job_id)
    if not safe_job:
        return None, safe_parent, safe_job, "Missing delegation job id. Use delegate_task with delegation_job_id."
    try:
        from core import get_workspace_path

        workspace = get_workspace_path()
    except Exception as exc:
        return None, safe_parent, safe_job, f"Unable to resolve workspace path: {exc}"
    rel = f"memory/delegation/{safe_parent}/{safe_job}.md"
    path, err = safe_resolve_path(workspace, rel, allow_write=allow_write, create_parents=allow_write)
    return path, safe_parent, safe_job, (err or "")


def get_limits() -> tuple[int, int]:
    cfg = _get_subagent_config()
    max_append_chars = int(cfg.get("delegation_scratch_max_append_chars", 12000) or 12000)
    max_file_bytes = int(cfg.get("delegation_scratch_max_file_bytes", 200000) or 200000)
    return max_append_chars, max_file_bytes


def get_handoff_auto_config() -> tuple[bool, int, bool]:
    cfg = _get_subagent_config()
    auto = bool(cfg.get("handoff_blackboard_auto", True))
    inject_chars = int(cfg.get("handoff_blackboard_max_inject_chars", 3500) or 3500)
    post_append = bool(cfg.get("handoff_blackboard_post_append", True))
    return auto, max(200, inject_chars), post_append


def _ensure_header(existing: str, *, safe_parent: str, safe_job: str) -> str:
    if existing.strip():
        return existing
    return (
        "# Delegation scratch\n\n"
        f"- parent_thread_id: {safe_parent}\n"
        f"- delegation_job_id: {safe_job}\n"
    )


def append_markdown_block(
    *,
    abs_path: str,
    safe_parent: str,
    safe_job: str,
    section: str,
    body: str,
    max_file_bytes: int,
) -> tuple[bool, str]:
    heading = sanitize_component((section or "").strip(), "update").replace("_", " ")
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    block = f"\n## {heading} ({timestamp})\n{body}\n"
    with _SCRATCH_WRITE_LOCK:
        existing = ""
        if abs_path and os.path.isfile(abs_path):
            try:
                with open(abs_path, "r", encoding="utf-8") as f:
                    existing = f.read()
            except Exception:
                existing = ""
        existing = _ensure_header(existing, safe_parent=safe_parent, safe_job=safe_job)
        new_content = existing.rstrip() + block
        if len(new_content.encode("utf-8")) > max_file_bytes:
            return False, (
                f"Delegation scratch limit reached ({max_file_bytes} bytes). "
                "Use a new delegation_job_id or summarize existing scratch first."
            )
        try:
            secure_write_file(abs_path, new_content.rstrip() + "\n", mode=0o600)
        except Exception as exc:
            return False, f"Failed to write delegation scratch: {exc}"
    return True, ""


def append_system_footer(
    *,
    abs_path: str,
    safe_parent: str,
    safe_job: str,
    status: str,
    elapsed_seconds: float,
    output_chars: int,
) -> tuple[bool, str]:
    body = (
        f"- status: {status}\n"
        f"- elapsed_seconds: {elapsed_seconds:.2f}\n"
        f"- output_chars: {max(0, int(output_chars))}\n"
    )
    return append_markdown_block(
        abs_path=abs_path,
        safe_parent=safe_parent,
        safe_job=safe_job,
        section="system footer",
        body=body,
        max_file_bytes=get_limits()[1],
    )


def read_blackboard_excerpt(abs_path: str, *, max_chars: int) -> str:
    if not abs_path or not os.path.isfile(abs_path):
        return ""
    try:
        with open(abs_path, "r", encoding="utf-8") as f:
            text = f.read()
    except Exception:
        return ""
    cap = max(200, int(max_chars or 0))
    if len(text) > cap:
        return text[:cap] + "\n\n[Truncated]"
    return text

