"""runId 빌드/파싱/세그먼트 escape, 세션명, container compose 라벨."""
from __future__ import annotations

import hashlib as _hashlib
import re as _re
from typing import Dict

from okstra_project.state import slugify as _slugify

# docker compose project name 길이 상한. compose 는 [a-z0-9][a-z0-9_-]* 만 허용한다.
CONTAINER_PROJECT_NAME_MAX = 60


# runId 형식: "<project-id>/<task-group>/<task-id>/<task-type>/r<run-seq>"
# task-type 이 포함되는 이유: okstra.sh 의 RUN_DIR 은 task-type 단위 하위 디렉터리이고
# 카테고리별 카운터(RUN_*_SEQ) 도 task-type 별로 독립이라, 같은 (group, task-id) 라도
# task-type 이 다르면 별개의 run 으로 취급해야 한다.
# run-seq 는 최소 2자리 zero-pad.
_RUN_ID_PATTERN = _re.compile(
    r"^(?P<projectId>[^/]+)/(?P<taskGroup>[^/]+)/(?P<taskId>[^/]+)"
    r"/(?P<taskType>[^/]+)/r(?P<runSeq>\d+)$"
)


def build_run_id(project_id: str, task_group: str, task_id: str,
                 task_type: str, run_seq: int) -> str:
    """runId 5-tuple 을 단일 슬러그 문자열로 합친다.
    각 세그먼트는 fs-safe 슬러그로 정규화한다 — 그렇지 않으면 task 식별자에
    `/` 가 섞였을 때 (group='a/b', task='c') 와 (group='a', task='b/c') 가
    동일 runId 'p/a/b/c/type/r01' 로 충돌해 중앙 인덱스에서 다른 run 을
    가리키거나 show/rerun 이 잘못된 run 을 대상으로 동작한다. okstra.sh 가
    프로젝트 로컬 디렉터리에서 사용하는 slugify_value 와 동일 규칙이라
    raw 매니페스트 값과 일관된 키를 얻는다.
    """
    seq = f"{run_seq:02d}" if run_seq < 100 else str(run_seq)
    return (f"{_safe_fs_segment(project_id)}/{_safe_fs_segment(task_group)}"
            f"/{_safe_fs_segment(task_id)}/{_safe_fs_segment(task_type)}/r{seq}")


def parse_run_id(run_id: str) -> Dict[str, object]:
    """runId 슬러그를 5-tuple dict 로 분해한다. 형식이 틀리면 ValueError."""
    match = _RUN_ID_PATTERN.match(run_id)
    if not match:
        raise ValueError(f"invalid runId: {run_id!r}")
    return {
        "projectId": match.group("projectId"),
        "taskGroup": match.group("taskGroup"),
        "taskId": match.group("taskId"),
        "taskType": match.group("taskType"),
        "runSeq": int(match.group("runSeq")),
    }


def _safe_fs_segment(value: str) -> str:
    """파일시스템 경로 컴포넌트로 사용하기 전에 정규화한다.
    task_group/task_id 등은 CLI 와 manifest 에서 들어오므로 검증되지 않은
    상태이며, `/` 나 `..` 가 들어 있으면 invocations 디렉터리 밖으로
    탈출해 다른 프로젝트의 central 트리를 오염시킬 수 있다. okstra.sh
    의 slugify_value 와 동일한 규칙(`[a-z0-9-]+`) 으로 강제 정규화해
    위험 문자를 모두 제거한다. 정규화 결과가 비어 있으면 `_` 로
    대체해 빈 path 컴포넌트가 만들어지는 것도 차단한다.
    """
    slug = slugify_task_segment(value)
    return slug if slug else "_"


def _escape_segment_for_join(segment: str) -> str:
    """세그먼트 내부의 `-` 를 `--` 로 escape 해 단일 `-` 조인 시에도
    원래 슬래시 경계를 손실 없이 복원할 수 있게 한다.
    슬러그 정규화 결과(`[a-z0-9-]+`) 와 호환되며, 디코더는 단일 `-` 가
    경계, 이중 `-` 가 세그먼트 내부 하이픈임을 식별할 수 있다.
    """
    return segment.replace("-", "--")


def run_id_to_session_name(run_id: str) -> str:
    """tmux 세션명으로 변환한다. 'okstra-' 접두사 + 세그먼트 escape 후 `-` 조인.
    예: 'a/b-c/r01' → 'okstra-a-b--c-r01'. 세그먼트 내부 `-` 는 `--` 로
    이스케이프되어, 슬래시 위치가 다른 두 runId 가 동일 세션명으로
    충돌하지 않는다. 세그먼트는 fs-safe 슬러그로 강제 정규화해
    `..` 같은 traversal 토큰이 세션명에 그대로 노출되지 않게 한다.
    """
    parts = [_escape_segment_for_join(_safe_fs_segment(seg))
             for seg in run_id.split("/")]
    return "okstra-" + "-".join(parts)


def slugify_task_segment(value: str) -> str:
    """Match okstra.sh slugify_value for task directory segments."""
    return _re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")


def build_container_session_name(project_id: str, group: str, task_id: str) -> str:
    """container detached tmux 세션명을 합친다 → 'okstra-container-<slug>'.

    세그먼트는 task_dir 가 쓰는 slugify_task_segment 와 동일 규칙으로 정규화해
    세션명 slug 가 프로젝트 디스크 경로 slug 와 분기하지 않게 한다.
    """
    seg = slugify_task_segment
    return f"okstra-container-{seg(project_id)}-{seg(group)}-{seg(task_id)}"


def compose_project_name(project_id: str, task_group: str, task_id: str) -> str:
    """docker compose project name 을 합친다(순수 계산).

    세그먼트 정규화는 ``paths.task_dir`` 가 쓰는 slug 함수(okstra_project.state.
    slugify)와 동일 함수를 써서 라벨(이 이름이 SSOT)↔디스크 경로의 slug 가
    분기하지 않게 한다. 상한(``CONTAINER_PROJECT_NAME_MAX``) 초과 시 읽기 좋은
    본문을 앞에서 자르고 끝에 전체 이름의 해시 8자리를 붙인다 — 단순 prefix-cut 은
    task_group 만 다른 두 task-key 를 같은 이름으로 collapse 시켜(라벨 SSOT 충돌로
    한 task 의 down 이 형제 task 컨테이너까지 회수) 위험하므로, 해시로 distinct 를
    보장하고 상한도 항상 만족시킨다."""
    def seg(v: str) -> str:
        return _slugify(v) or "x"  # 빈 슬러그 방지(compose 는 [a-z0-9] 시작 요구)

    proj, grp, task = seg(project_id), seg(task_group), seg(task_id)
    name = f"okstra-{proj}-{grp}-{task}"
    if len(name) <= CONTAINER_PROJECT_NAME_MAX:
        return name
    digest = _hashlib.sha1(name.encode("utf-8")).hexdigest()[:8]
    body_budget = CONTAINER_PROJECT_NAME_MAX - len("okstra-") - 1 - len(digest)
    body = f"{proj}-{grp}-{task}"[:body_budget].rstrip("-")
    return f"okstra-{body}-{digest}"
