"""Task ID parsing utilities (V2).

Supports unified parsing of task IDs across files, branches, and CLI args.

V2 pattern:
    task-<digits>(_<phase>)?(_<parallel>)?(+<retry>)?
    - phase:    \\d+\\.\\d+   (e.g. _1.2)
    - parallel: [a-z]         (e.g. _a)
    - retry:    \\d+           (e.g. +1)

Examples
--------
- ``task-2469``                 -> base only
- ``task-2469+1``               -> retry suffix preserved
- ``task-2469_1.2``             -> phase
- ``task-2469_1.2_a``           -> phase + parallel
- ``task-2469_1.2_a+3``         -> phase + parallel + retry
"""

from __future__ import annotations

import re
from typing import Optional

# Full V2 pattern (anchored). Captures: base id, phase, parallel, retry.
TASK_ID_V2_PATTERN = re.compile(
    r"^task-(?P<num>\d+)(?:_(?P<phase>\d+\.\d+))?(?:_(?P<parallel>[a-z]))?(?:\+(?P<retry>\d+))?$"
)

# Public alias: compiled re.Pattern for external consumers.
TASK_ID_RE: re.Pattern = TASK_ID_V2_PATTERN

# Public alias: V2 pattern as a string constant.
TASK_ID_PATTERN: str = r"^task-\d+(?:_\d+\.\d+)?(?:_[a-z])?(?:\+\d+)?$"

# Legacy dot-phase 호환 패턴 (task-2502: PR #47 partial merge 회복).
# 회귀 evidence: PR #49/#50/#51 Gemini high — task-1234.5 거부 회귀 픽스용.
# legacy 예시: task-9.1, task-1234.5, task-648.1.dev1, task-648.1.dev1.done
_LEGACY_DOTPHASE_PATTERN = re.compile(
    r"^task-\d+(?:\.\d+)*(?:\.dev\d+)?(?:\.done)?$"
)

# Loose pattern used when scanning filenames/branches.
# 경계 강화 (task-2485 hardening):
#   - 뒤쪽 경계(?![0-9+_a-z])로 ``task-2472+10`` 가 ``task-2472+1`` 로 잘리거나
#     ``task-24720`` 이 ``task-2472`` 로 잘리는 사고 방지.
#   - 앞쪽 경계(?<![\w/-])로 ``foo-task-2472-bar`` 같이 단어/경로 중간의 거짓 매치 차단.
_TASK_ID_LOOSE_PATTERN = re.compile(
    # extractor 용 — 앞쪽 경계 없이 단어 중간/하이픈 뒤에서도 추출 가능 (예: ``foo-task-2472+1-bar``).
    # 뒤쪽 경계만 강제하여 ``task-2472+10`` 이 ``task-2472+1`` 로 잘리는 사고를 방어한다.
    r"task-(?P<num>\d+)"
    r"(?:_(?P<phase>\d+\.\d+))?"
    r"(?:_(?P<parallel>[a-z]))?"
    r"(?:\+(?P<retry>\d+))?"
    r"(?![0-9+_a-z])"  # 뒤쪽 경계 (계속되는 숫자/+/_/소문자 차단)
)

# strict pattern — file path/identifier 단어 단위 매칭 용.
# 앞뒤 모두 경계를 두어 ``foo-task-2472-bar`` 같은 단어 중간 거짓매치를 차단한다.
# ``_filter_dirty_to_task_scope`` 등 ``다른 task scope 와의 분리 판단`` 자리에서 사용.
_TASK_ID_STRICT_PATTERN = re.compile(
    r"(?<![A-Za-z0-9_\-])"  # 앞쪽 경계: 영숫자/언더스코어/하이픈 직후 매치 차단
    r"task-(?P<num>\d+)"
    r"(?:_(?P<phase>\d+\.\d+))?"
    r"(?:_(?P<parallel>[a-z]))?"
    r"(?:\+(?P<retry>\d+))?"
    r"(?![0-9+_a-z])"
)


# 외부 (예: ``teams/shared/verifiers/git_evidence.py`` ``_filter_dirty_to_task_scope``)
# 가 strict 경계 매칭을 위해 import 하므로 Pyright unused 경고 회피용 더미 참조.
__all_extra_exports__ = (_TASK_ID_STRICT_PATTERN,)


def parse_task_id_v2(s: str) -> dict:
    """Parse a V2 task id string into structural components.

    Returns a dict with keys ``base``, ``phase``, ``parallel``, ``retry``.
    For inputs that don't match, returns dict with all values ``None``.

    Examples
    --------
    >>> sorted(parse_task_id_v2("task-2469+1").items())
    [('base', 'task-2469'), ('num', '2469'), ('parallel', None), ('phase', None), ('retry', '+1')]
    """
    m = TASK_ID_V2_PATTERN.match(s.strip())
    if not m:
        return {"base": None, "phase": None, "parallel": None, "retry": None, "num": None}

    base = f"task-{m.group('num')}"
    phase = f"_{m.group('phase')}" if m.group("phase") else None
    parallel = f"_{m.group('parallel')}" if m.group("parallel") else None
    retry = f"+{m.group('retry')}" if m.group("retry") else None

    # Gemini PR #47 high #1 대응: 호출자가 num 을 직접 사용할 수 있도록 raw num 도 반환.
    # 기존 호출자(``base``/``phase``/``parallel``/``retry``)는 영향 없음 (필드 추가만).
    return {"base": base, "phase": phase, "parallel": parallel, "retry": retry, "num": m.group("num")}


def is_valid_task_id(s: str) -> bool:
    """Return True iff ``s`` matches the V2 pattern exactly."""
    return TASK_ID_V2_PATTERN.match(s.strip()) is not None


def is_valid_task_id_with_legacy(s: object) -> bool:
    """V2 task id 또는 legacy dot-phase task id를 PASS.

    legacy 예시: task-9.1, task-1234.5, task-648.1.dev1, task-648.1.dev1.done
    V2 예시:    task-2485+1, task-2487+1, task-2469_1.2_a+3
    """
    if not isinstance(s, str):
        return False
    s = s.strip()
    return bool(TASK_ID_V2_PATTERN.match(s)) or bool(_LEGACY_DOTPHASE_PATTERN.match(s))


def _reassemble_from_match(m: re.Match) -> str:
    parts = [f"task-{m.group('num')}"]
    if m.group("phase"):
        parts.append(f"_{m.group('phase')}")
    if m.group("parallel"):
        parts.append(f"_{m.group('parallel')}")
    if m.group("retry"):
        parts.append(f"+{m.group('retry')}")
    return "".join(parts)


def extract_task_id_from_filename(filename: str) -> Optional[str]:
    """Extract a task id from a filename or path.

    Examples
    --------
    >>> extract_task_id_from_filename("memory/tasks/task-2469+1.md")
    'task-2469+1'
    >>> extract_task_id_from_filename("task-2469_1.2_a+3.md")
    'task-2469_1.2_a+3'
    """
    if not isinstance(filename, str) or not filename:
        return None

    # Strip directory + extension to maximize chance of clean match.
    basename = filename.rsplit("/", 1)[-1]
    stem = basename.rsplit(".", 1)[0] if "." in basename else basename

    m = TASK_ID_V2_PATTERN.match(stem)
    if m:
        return _reassemble_from_match(m)

    # Fallback: search inside the original (helps for prefixes like "memo_task-2469.md").
    m = _TASK_ID_LOOSE_PATTERN.search(filename)
    if m:
        return _reassemble_from_match(m)

    return None


def extract_task_id_from_branch(branch: str) -> Optional[str]:
    """Extract a task id from a branch name.

    Examples
    --------
    >>> extract_task_id_from_branch("task/task-2467+3-dev6")
    'task-2467+3'
    >>> extract_task_id_from_branch("task/task-2469_1.2-dev2")
    'task-2469_1.2'
    """
    if not isinstance(branch, str) or not branch:
        return None

    m = _TASK_ID_LOOSE_PATTERN.search(branch)
    if not m:
        return None

    return _reassemble_from_match(m)


def extract_task_id(text: str) -> Optional[str]:
    """Extract the first task id from arbitrary text (branch name, PR title, body, etc.).

    Uses the loose (unanchored) pattern to find a task id embedded anywhere in
    the input string.

    Examples
    --------
    >>> extract_task_id("foo-task-2472+1-bar")
    'task-2472+1'
    >>> extract_task_id("fix issue task-2467+3 in CI")
    'task-2467+3'
    >>> extract_task_id("no task here")
    """
    if not isinstance(text, str) or not text:
        return None

    m = _TASK_ID_LOOSE_PATTERN.search(text)
    if not m:
        return None

    return _reassemble_from_match(m)
