# -*- coding: utf-8 -*-
"""ContextAttachmentResolver @file 引用解析器（Phase 7 Tier 1 BD）。

借鉴 zhikuncode ContextAttachmentResolver.java + IDE 引用语法：
- @file.py            引用整个文件（受最大行数限制）
- @file.py:L10        引用第 10 行
- @file.py:L10-L20    引用第 10-20 行

CLI 集成场景：
- 用户消息 "看下 @src/main.py:L10-L20 的实现" 自动展开为代码块
- 减少用户手动复制粘贴
- 在 main.py 接收 user input 后、写入 session 前调用

实现要点：
- 路径校验复用 PathSecurityService
- 大文件截断（默认 500 行）
- 失败优雅降级（保留原引用 + 错误说明）
"""
from __future__ import annotations

import os
import re
import threading
from dataclasses import dataclass, field
from typing import List, Optional

from ..security.path_security import get_default_service as get_path_security

__all__ = [
    "ContextAttachmentResolver",
    "Attachment",
    "get_default_resolver",
]


# 引用语法正则：
# @path/to/file.ext:L10-L20  或  @path/to/file.ext:L10-L20  或  @path/to/file.ext
# 路径支持字母数字 _ - . / \，扩展名至少 1 字符
# 行号格式：L<n>、L<n>-L<m>、L<n>-<m>
_REFERENCE_PATTERN = re.compile(
    r"@(?P<path>[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+)"  # 文件路径（含扩展名）
    r"(?::L(?P<start>\d+)(?:-L?(?P<end>\d+))?)?"  # 可选 :L10 / :L10-L20 / :L10-20
)

# 默认最大行数（防止巨文件撑爆上下文）
DEFAULT_MAX_LINES = 500


@dataclass
class Attachment:
    """单条 @file 引用解析结果。"""

    path: str
    start_line: Optional[int] = None
    end_line: Optional[int] = None
    raw: str = ""  # 原始匹配字符串


class ContextAttachmentResolver:
    """解析并展开用户消息中的 @file:L10-L20 引用。

    Usage:
        >>> r = ContextAttachmentResolver(working_dir="/path/to/project")
        >>> text = "看下 @src/main.py:L10-L20 的实现"
        >>> expanded = r.resolve(text)
    """

    def __init__(
        self,
        working_dir: Optional[str] = None,
        max_lines: int = DEFAULT_MAX_LINES,
    ) -> None:
        self._working_dir = working_dir
        self._max_lines = max_lines
        self._lock = threading.Lock()

    def set_working_dir(self, path: Optional[str]) -> None:
        with self._lock:
            self._working_dir = path

    # ------------------------------------------------------------------ public API

    def parse_references(self, text: str) -> List[Attachment]:
        """解析文本中所有 @file[:L10[-L20]] 引用。"""
        if not text:
            return []
        refs = []
        # 排除邮箱：路径必须含 . 且后面是扩展名（字母数字），不是域名
        for match in _REFERENCE_PATTERN.finditer(text):
            path = match.group("path")
            # 排除邮箱：路径不应以 . 开头（域名）且不应有 @ 之前的字符
            # 用 lookbehind 排除 user@domain 形式
            start_pos = match.start()
            if start_pos > 0 and text[start_pos - 1] not in (" ", "\n", "\t", "(", "[", ":", "、", "，", "。"):
                continue
            start_str = match.group("start")
            end_str = match.group("end")
            if start_str:
                start_line = int(start_str)
                end_line = int(end_str) if end_str else start_line
            else:
                start_line = None
                end_line = None
            refs.append(Attachment(
                path=path,
                start_line=start_line,
                end_line=end_line,
                raw=match.group(0),
            ))
        return refs

    def resolve(self, text: str) -> str:
        """展开文本中所有 @file 引用为代码块。

        未找到的文件保留原引用并附错误说明。
        """
        if not text:
            return text

        refs = self.parse_references(text)
        if not refs:
            return text

        result = text
        for ref in refs:
            expanded = self._resolve_one(ref)
            result = result.replace(ref.raw, expanded, 1)
        return result

    # ------------------------------------------------------------------ 内部方法

    def _resolve_one(self, ref: Attachment) -> str:
        """解析单条引用为代码块字符串。"""
        # 路径安全校验
        sec = get_path_security()
        check = sec.check_read(ref.path)
        if not check.allowed and not check.needs_confirmation:
            return f"[ERR 引用 {ref.raw} 路径不安全: {check.reason}]"

        # 解析为绝对路径
        path = self._resolve_path(ref.path)
        if path is None or not os.path.exists(path):
            return f"[ERR 引用 {ref.raw} 文件不存在]"

        try:
            with open(path, "r", encoding="utf-8", errors="replace") as f:
                lines = f.readlines()
        except (OSError, UnicodeDecodeError) as e:
            return f"[ERR 引用 {ref.raw} 读取失败: {e}]"

        total_lines = len(lines)

        # 确定行范围
        if ref.start_line is not None:
            start = max(0, ref.start_line - 1)
            end = ref.end_line if ref.end_line is not None else ref.start_line
            end = min(end, total_lines)  # 超出范围截断
        else:
            start = 0
            end = total_lines

        # 截断保护
        truncated = False
        if end - start > self._max_lines:
            end = start + self._max_lines
            truncated = True

        snippet = "".join(lines[start:end])

        # 构建代码块
        if ref.start_line is None:
            line_range_str = ""
        elif ref.end_line is None or ref.start_line == ref.end_line:
            line_range_str = f":L{ref.start_line}"
        else:
            line_range_str = f":L{ref.start_line}-L{ref.end_line}"
        header = f"📎 {ref.path}{line_range_str}"
        if truncated:
            header += f" (截断前 {self._max_lines} 行 / 共 {total_lines} 行)"

        # 提取扩展名用于代码块语言标记
        ext = os.path.splitext(ref.path)[1].lstrip(".")
        return f"{header}\n```{ext}\n{snippet}```\n"

    def _resolve_path(self, path: str) -> Optional[str]:
        """解析路径为绝对路径。相对路径基于 working_dir。"""
        try:
            if os.path.isabs(path):
                return path
            if self._working_dir is None:
                # 默认基于进程 CWD
                return os.path.abspath(path)
            return os.path.join(self._working_dir, path)
        except (OSError, ValueError):
            return None


# ------------------------------------------------------------------ 模块级单例

_default_resolver: Optional[ContextAttachmentResolver] = None
_default_lock = threading.Lock()


def get_default_resolver() -> ContextAttachmentResolver:
    """返回模块级默认 ContextAttachmentResolver 单例。"""
    global _default_resolver
    if _default_resolver is None:
        with _default_lock:
            if _default_resolver is None:
                _default_resolver = ContextAttachmentResolver()
    return _default_resolver
