"""
Memory Check Confirmation Number System (MC)

매 task 위임 시 MC-XXXX를 발급하여 메모리 읽기 여부를 추적한다.
MEMORY.md의 ★ 항목과 관련 피드백 파일 목록을 로그에 기록한다.
"""

import fcntl
import hashlib
import json
import os
import re
from datetime import datetime
from pathlib import Path
from typing import Any

try:
    from utils.logger import get_logger

    logger = get_logger(__name__)
except ImportError:
    import logging

    logger = logging.getLogger(__name__)

# 기본 경로 상수
_WORKSPACE = Path(os.environ.get("WORKSPACE_ROOT", "/home/jay/workspace"))
_DEFAULT_MEMORY_PATH = _WORKSPACE / "memory" / "MEMORY.md"
_DEFAULT_LOG_PATH = _WORKSPACE / "memory" / "memory-check-log.json"
_DEFAULT_TIMERS_PATH = _WORKSPACE / "memory" / "task-timers.json"

# Anu Claude auto-memory 경로 (피드백 파일 위치)
_ANU_MEMORY_DIR = Path(
    os.environ.get(
        "MEMORY_CHECK_FEEDBACK_DIR",
        str(Path.home() / ".claude" / "projects" / "-home-jay--cokacdir-workspace-autoset" / "memory"),
    )
)
_ANU_MEMORY_PATH = _ANU_MEMORY_DIR / "MEMORY.md"


def parse_star_items(memory_path: Path) -> list[str]:
    """memory_path 파일에서 ★ 포함 라인을 추출하고 마크다운 서식을 제거한다.

    Args:
        memory_path: 읽을 MEMORY.md 파일 경로

    Returns:
        ★ 항목 문자열 리스트 (마크다운 서식 제거됨). 파일 없거나 에러 시 빈 리스트.
    """
    try:
        if not memory_path.exists():
            return []
        content = memory_path.read_text(encoding="utf-8")
    except Exception as e:
        logger.debug(f"[parse_star_items] 파일 읽기 실패 ({memory_path}): {e}")
        return []

    results: list[str] = []
    try:
        for line in content.split("\n"):
            if "★" not in line:
                continue
            # 마크다운 서식 제거
            cleaned = line.strip()
            # 헤딩 기호 제거 (##, ###, ...)
            cleaned = re.sub(r"^#{1,6}\s*", "", cleaned)
            # 볼드/이탤릭 제거 (**, __, *, _)
            cleaned = re.sub(r"\*{1,3}([^*]*)\*{1,3}", r"\1", cleaned)
            cleaned = re.sub(r"_{1,3}([^_]*)_{1,3}", r"\1", cleaned)
            # 취소선 제거 (~~text~~)
            cleaned = re.sub(r"~~([^~]*)~~", r"\1", cleaned)
            # 인라인 코드 제거 (`code`)
            cleaned = re.sub(r"`([^`]*)`", r"\1", cleaned)
            # 링크 문법 제거 ([text](url) → text)
            cleaned = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", cleaned)
            # 이미지 문법 제거 (![alt](url) → alt)
            cleaned = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", cleaned)
            # 리스트 기호 제거 (-, *, +)
            cleaned = re.sub(r"^[-*+]\s+", "", cleaned)
            # 번호 리스트 제거 (1. 2. ...)
            cleaned = re.sub(r"^\d+\.\s+", "", cleaned)
            # 연속 공백 정리
            cleaned = re.sub(r"\s+", " ", cleaned).strip()
            if cleaned:
                results.append(cleaned)
    except Exception as e:
        logger.debug(f"[parse_star_items] 파싱 중 에러 ({memory_path}): {e}")
        return []

    return results


def find_feedback_files(feedback_dir: Path) -> list[str]:
    """feedback_dir에서 feedback_*.md 패턴에 맞는 파일명 리스트를 반환한다.

    Args:
        feedback_dir: 피드백 파일이 있는 디렉토리 경로

    Returns:
        파일명 리스트 (경로 없이 파일명만). 디렉토리 없거나 에러 시 빈 리스트.
    """
    try:
        if not feedback_dir.exists():
            return []
        return sorted(p.name for p in feedback_dir.glob("feedback_*.md"))
    except Exception as e:
        logger.debug(f"[find_feedback_files] 디렉토리 읽기 실패 ({feedback_dir}): {e}")
        return []


def match_feedback_to_task(task_desc: str, feedback_dir: Path) -> list[str]:
    """task_desc 내용에 키워드 매칭되는 피드백 파일 목록을 반환한다.

    파일명에서 키워드를 추출하여 task_desc와 2개 이상 매칭 시 포함.
    키워드가 1개인 짧은 파일명은 1개 매칭으로도 포함.

    Args:
        task_desc: 작업 설명 문자열
        feedback_dir: 피드백 파일 디렉토리 경로

    Returns:
        매칭된 피드백 파일명 리스트
    """
    try:
        feedback_files = find_feedback_files(feedback_dir)
        if not feedback_files:
            return []

        task_lower = task_desc.lower()
        matched: list[str] = []

        for fname in feedback_files:
            # feedback_ 접두사 제거, .md 확장자 제거
            stem = fname
            if stem.startswith("feedback_"):
                stem = stem[len("feedback_") :]
            if stem.endswith(".md"):
                stem = stem[:-3]

            # _로 분할 후 숫자/버전 제거 (v2, v3, 숫자만인 토큰 등)
            parts = stem.split("_")
            keywords = [p for p in parts if p and not re.match(r"^v?\d+$", p, re.IGNORECASE)]

            if not keywords:
                continue

            # task_desc에서 키워드 매칭 카운트
            match_count = sum(1 for kw in keywords if kw in task_lower)

            # 키워드가 1개뿐인 짧은 파일명은 1개 매칭으로도 포함
            threshold = 1 if len(keywords) <= 1 else 2
            if match_count >= threshold:
                matched.append(fname)

        return matched
    except Exception as e:
        logger.debug(f"[match_feedback_to_task] 매칭 중 에러: {e}")
        return []


def get_next_mc_id(log_path: Path) -> str:
    """log_path의 JSON 파일에서 다음 MC ID를 계산하여 반환한다.

    checks 배열의 마지막 mc_id에서 숫자를 추출해 +1 한다.

    Args:
        log_path: memory-check-log.json 파일 경로

    Returns:
        "MC-XXXX" 형식의 ID 문자열 (제로패딩 4자리, 9999 초과 시 그대로 증가)
    """
    try:
        if not log_path.exists():
            return "MC-0001"

        with open(log_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        checks = data.get("checks", [])
        if not checks:
            return "MC-0001"

        last_mc_id = checks[-1].get("mc_id", "MC-0000")
        match = re.search(r"MC-(\d+)", last_mc_id)
        if not match:
            return "MC-0001"

        next_num = int(match.group(1)) + 1
        # 4자리 패딩 (9999 초과 시 그대로 증가)
        if next_num <= 9999:
            return f"MC-{next_num:04d}"
        else:
            return f"MC-{next_num}"
    except Exception as e:
        logger.debug(f"[get_next_mc_id] MC ID 계산 실패: {e}")
        return "MC-0001"


def issue_mc(
    task_id: str,
    task_desc: str,
    log_path: Path | None = None,
    memory_path: Path | None = None,
    feedback_dir: Path | None = None,
    *,
    _skip_anu_memory: bool = False,
) -> dict:
    """MC(Memory Check Confirmation Number)를 발급하고 로그에 기록한다.

    워크스페이스 MEMORY.md와 Anu MEMORY.md에서 ★ 항목을 파싱하고,
    관련 피드백 파일을 매칭하여 로그에 기록한다.

    Args:
        task_id: 태스크 ID (예: "task-1454.1")
        task_desc: 작업 설명
        log_path: 로그 파일 경로 (기본: _DEFAULT_LOG_PATH)
        memory_path: 워크스페이스 MEMORY.md 경로 (기본: _DEFAULT_MEMORY_PATH)
        feedback_dir: 피드백 파일 디렉토리 (기본: _ANU_MEMORY_DIR)

    Returns:
        {"mc_id": "MC-XXXX", "task_id": "...", "timestamp": "...",
         "star_items_checked": N, "memory_items_read": [...]}
        에러 시 빈 dict 반환
    """
    _custom_memory = memory_path is not None
    if log_path is None:
        log_path = _DEFAULT_LOG_PATH
    if memory_path is None:
        memory_path = _DEFAULT_MEMORY_PATH
    if feedback_dir is None:
        feedback_dir = _ANU_MEMORY_DIR

    lock_path = log_path.parent / ".memory-check-log.lock"
    lock_fd = None

    try:
        log_path.parent.mkdir(parents=True, exist_ok=True)

        lock_fd = open(lock_path, "w")
        fcntl.flock(lock_fd, fcntl.LOCK_EX)

        # 1. 워크스페이스 MEMORY.md ★ 항목 파싱
        ws_star_items = parse_star_items(memory_path)

        # 2. Anu MEMORY.md ★ 항목 파싱 (기본 경로 사용 시에만, 커스텀 경로면 스킵)
        anu_star_items: list[str] = []
        if not _custom_memory and not _skip_anu_memory and _ANU_MEMORY_PATH.exists():
            anu_star_items = parse_star_items(_ANU_MEMORY_PATH)

        # 3. 두 소스의 ★ 항목 합치기 (중복 제거, 순서 유지)
        seen: set[str] = set()
        all_star_items: list[str] = []
        for item in ws_star_items + anu_star_items:
            if item not in seen:
                seen.add(item)
                all_star_items.append(item)

        # 4. 피드백 파일 매칭
        matched_feedback = match_feedback_to_task(task_desc, feedback_dir)

        # 5. MC-XXXX 발급
        mc_id = get_next_mc_id(log_path)

        # 6. 로그 파일에 기록
        timestamp = datetime.now().isoformat()

        if log_path.exists():
            try:
                with open(log_path, "r", encoding="utf-8") as f:
                    log_data = json.load(f)
            except (json.JSONDecodeError, OSError):
                log_data = {"checks": []}
        else:
            log_data = {"checks": []}

        if "checks" not in log_data:
            log_data["checks"] = []

        memory_items_read = all_star_items + matched_feedback

        # ── task-2419: memory_items_structured 빌드 ──
        memory_items_structured: list[dict[str, Any]] = []

        # ws_star_items → source_path: "memory/MEMORY.md"
        for item in ws_star_items:
            content_preview = item[:100]
            source_path = "memory/MEMORY.md"
            item_hash = hashlib.sha256((source_path + content_preview).encode("utf-8")).hexdigest()[:12]
            memory_items_structured.append(
                {
                    "memory_item_id": item_hash,
                    "source_path": source_path,
                    "item_type": "star_item",
                    "content_preview": content_preview,
                    "ack_required": True,
                }
            )

        # anu_star_items → source_path: "anu_memory/MEMORY.md"
        for item in anu_star_items:
            content_preview = item[:100]
            source_path = "anu_memory/MEMORY.md"
            item_hash = hashlib.sha256((source_path + content_preview).encode("utf-8")).hexdigest()[:12]
            memory_items_structured.append(
                {
                    "memory_item_id": item_hash,
                    "source_path": source_path,
                    "item_type": "star_item",
                    "content_preview": content_preview,
                    "ack_required": True,
                }
            )

        # matched_feedback 파일명 → source_path: "anu_memory/{filename}"
        for fname in matched_feedback:
            content_preview = fname[:100]
            source_path = f"anu_memory/{fname}"
            item_hash = hashlib.sha256((source_path + content_preview).encode("utf-8")).hexdigest()[:12]
            memory_items_structured.append(
                {
                    "memory_item_id": item_hash,
                    "source_path": source_path,
                    "item_type": "feedback_file",
                    "content_preview": content_preview,
                    "ack_required": True,
                }
            )

        entry: dict[str, Any] = {
            "mc_id": mc_id,
            "task_id": task_id,
            "timestamp": timestamp,
            "star_items_checked": len(all_star_items),
            "memory_items_read": memory_items_read,  # 평면 string 배열 (기존, 회귀 0)
            # ── task-2419 추가 필드 ──
            "pending": True,
            "acknowledged": False,
            "acked_at": None,
            "memory_items_structured": memory_items_structured,
        }
        if matched_feedback:
            entry["matched_feedback_files"] = matched_feedback

        log_data["checks"].append(entry)

        with open(log_path, "w", encoding="utf-8") as f:
            json.dump(log_data, f, ensure_ascii=False, indent=2)

        logger.debug(
            f"[issue_mc] {mc_id} 발급: task_id={task_id}, "
            f"★항목={len(all_star_items)}개, "
            f"피드백={len(matched_feedback)}개"
        )

        return {
            "mc_id": mc_id,
            "task_id": task_id,
            "timestamp": timestamp,
            "star_items_checked": len(all_star_items),
            "memory_items_read": memory_items_read,
            "pending": True,
            "acknowledged": False,
            "memory_items_structured": memory_items_structured,
        }

    except Exception as e:
        logger.warning(f"[issue_mc] MC 발급 실패 (무시): {e}")
        return {}
    finally:
        if lock_fd is not None:
            try:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
                lock_fd.close()
            except Exception:
                pass


def get_unchecked_tasks(
    log_path: Path | None = None,
    timers_path: Path | None = None,
) -> list[dict]:
    """running 상태이지만 MC가 발급되지 않은 태스크 목록을 반환한다.

    Args:
        log_path: memory-check-log.json 경로 (기본: _DEFAULT_LOG_PATH)
        timers_path: task-timers.json 경로 (기본: _DEFAULT_TIMERS_PATH)

    Returns:
        [{"task_id": "task-1454.1", "team_id": "dev1-team"}, ...]
        에러 시 빈 리스트 반환
    """
    if log_path is None:
        log_path = _DEFAULT_LOG_PATH
    if timers_path is None:
        timers_path = _DEFAULT_TIMERS_PATH

    try:
        # task-timers.json에서 running 상태 태스크 추출
        running_tasks: list[dict] = []
        if timers_path.exists():
            with open(timers_path, "r", encoding="utf-8") as f:
                timers_data = json.load(f)
            for task_id, task_entry in timers_data.get("tasks", {}).items():
                if task_entry.get("status") == "running":
                    running_tasks.append(
                        {
                            "task_id": task_id,
                            "team_id": task_entry.get("team_id", ""),
                        }
                    )

        if not running_tasks:
            return []

        # memory-check-log.json에서 MC가 발급된 task_id 집합 추출
        checked_task_ids: set[str] = set()
        if log_path.exists():
            try:
                with open(log_path, "r", encoding="utf-8") as f:
                    log_data = json.load(f)
                for check in log_data.get("checks", []):
                    tid = check.get("task_id")
                    if tid:
                        checked_task_ids.add(tid)
            except (json.JSONDecodeError, OSError) as e:
                logger.debug(f"[get_unchecked_tasks] 로그 파일 읽기 실패: {e}")

        # running 태스크 중 MC가 없는 것 반환
        return [t for t in running_tasks if t["task_id"] not in checked_task_ids]

    except Exception as e:
        logger.debug(f"[get_unchecked_tasks] 조회 실패: {e}")
        return []


def ack_mc(mc_id: str, log_path: Path | None = None) -> bool:
    """MC를 ack 처리한다. pending=False, acknowledged=True, acked_at=now 설정.

    Args:
        mc_id: ack 대상 MC ID (예: "MC-0042")
        log_path: 로그 파일 경로 (기본: _DEFAULT_LOG_PATH)

    Returns:
        성공 시 True, 실패 또는 mc_id 미발견 시 False
    """
    if log_path is None:
        log_path = _DEFAULT_LOG_PATH

    lock_path = log_path.parent / ".memory-check-log.lock"
    lock_fd = None
    try:
        lock_fd = open(lock_path, "w")
        fcntl.flock(lock_fd, fcntl.LOCK_EX)

        if not log_path.exists():
            return False
        with open(log_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        for entry in data.get("checks", []):
            if entry.get("mc_id") == mc_id:
                entry["pending"] = False
                entry["acknowledged"] = True
                entry["acked_at"] = datetime.now().isoformat()
                with open(log_path, "w", encoding="utf-8") as f:
                    json.dump(data, f, ensure_ascii=False, indent=2)
                return True
        return False
    except Exception as e:
        logger.warning(f"[ack_mc] 실패: {e}")
        return False
    finally:
        if lock_fd is not None:
            try:
                fcntl.flock(lock_fd, fcntl.LOCK_UN)
                lock_fd.close()
            except Exception:
                pass


def get_pending_mcs(log_path: Path | None = None) -> list[dict]:
    """ack되지 않은 MC entries 반환.

    Returns:
        [{"mc_id": "...", "task_id": "...", "timestamp": "...", ...}, ...]
    """
    if log_path is None:
        log_path = _DEFAULT_LOG_PATH
    if not log_path.exists():
        return []
    try:
        with open(log_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        return [e for e in data.get("checks", []) if e.get("pending", False) is True]
    except Exception:
        return []


def get_mc_by_task(task_id: str, log_path: Path | None = None) -> dict | None:
    """task_id에 해당하는 가장 최근 MC entry 반환.

    Returns:
        entry dict 또는 None (미발견)
    """
    if log_path is None:
        log_path = _DEFAULT_LOG_PATH
    if not log_path.exists():
        return None
    try:
        with open(log_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        matches = [e for e in data.get("checks", []) if e.get("task_id") == task_id]
        return matches[-1] if matches else None
    except Exception:
        return None
