import hashlib
import threading
import time
from collections.abc import Mapping
from typing import TypeAlias

from sari.mcp.tools._util import (
    mcp_response,
    pack_header,
    pack_line,
    pack_encode_id,
    pack_encode_text,
    pack_error,
    ErrorCode,
    resolve_db_path,
    resolve_fs_path,
    resolve_root_ids,
    invalid_args_response,
)

from sari.core.queue_pipeline import DbTask

ToolResult: TypeAlias = dict[str, object]


def _parse_path_range(path: str, start_line: int | None, end_line: int | None) -> tuple[str, int | None, int | None]:
    """'path:start-end' 형식의 문자열로부터 경로와 라인 범위를 파싱합니다."""
    if ":" in path and (start_line is None and end_line is None):
        base, rng = path.rsplit(":", 1)
        if "-" in rng:
            a, b = rng.split("-", 1)
            try:
                return base, int(a), int(b)
            except Exception:
                return path, start_line, end_line
    return path, start_line, end_line


def _read_lines(db: object, db_path: str, roots: list[str]) -> list[str]:
    """DB 또는 파일 시스템에서 파일의 모든 라인을 읽어옵니다."""
    fs_path = resolve_fs_path(db_path, roots)
    if fs_path:
        try:
            with open(fs_path, "r", encoding="utf-8", errors="ignore") as f:
                return f.read().splitlines()
        except Exception:
            pass
    raw = db.read_file_raw(db_path) if hasattr(db, "read_file_raw") else db.read_file(db_path)
    return (raw or "").splitlines()


def _split_db_path_with_roots(db_path: str, roots: list[str]) -> tuple[str, str]:
    """Resolve (root_id, rel_path) even when root_id itself contains slashes."""
    for root_id in sorted(resolve_root_ids(roots), key=len, reverse=True):
        prefix = f"{root_id}/"
        if db_path == root_id:
            return root_id, ""
        if db_path.startswith(prefix):
            return root_id, db_path[len(prefix) :]
    if "/" in db_path:
        root_id, rel = db_path.split("/", 1)
        return root_id, rel
    return "", db_path


def _enqueue_or_write(db: object, indexer: object, row: tuple[object, ...]) -> None:
    """인덱서가 활성화된 경우 작업을 큐에 넣고, 그렇지 않으면 DB에 직접 씁니다."""
    writer = getattr(indexer, "_db_writer", None) if indexer else None
    if writer and DbTask:
        writer.enqueue(DbTask(kind="upsert_snippets", snippet_rows=[row]))
        return
    # 직접 쓰기 (CLI 또는 인덱서 미사용 시)
    prev = getattr(db, "_writer_thread_id", None)
    db.register_writer_thread(threading.get_ident())
    try:
        with db._lock:
            cur = db._write.cursor()
            db.upsert_snippet_tx(cur, [row])
            db._write.commit()
    finally:
        db.register_writer_thread(prev)


def build_save_snippet(
    args: Mapping[str, object],
    db: object,
    roots: list[str],
    indexer: object = None,
) -> ToolResult:
    """스니펫 저장을 위한 비즈니스 로직을 수행하고 결과 페이로드를 생성합니다."""
    path = str(args.get("path") or "").strip()
    tag = str(args.get("tag") or "").strip()
    start_line = args.get("start_line")
    end_line = args.get("end_line")
    note = str(args.get("note") or "").strip()
    commit_hash = str(args.get("commit") or "").strip()
    
    if not path or not tag:
        raise ValueError("path and tag are required")

    path, start_line, end_line = _parse_path_range(path, start_line, end_line)
    db_path = resolve_db_path(path, roots)
    if not db_path:
        raise ValueError("path is out of workspace scope")

    if start_line is None or end_line is None:
        raise ValueError("start_line and end_line are required")

    # 스니펫 내용 획득 및 앵커(Anchor) 추출
    lines = _read_lines(db, db_path, roots)
    start = max(1, int(start_line))
    end = max(start, int(end_line))
    snippet = "\n".join(lines[start - 1 : end])
    content_hash = hashlib.sha1(snippet.encode("utf-8")).hexdigest()
    anchor_before = lines[start - 2].strip() if start > 1 and len(lines) >= start - 1 else ""
    anchor_after = lines[end].strip() if end < len(lines) else ""

    root_id, rel = _split_db_path_with_roots(db_path, roots)
    if not root_id:
        raise ValueError("failed to resolve root_id from path")
    repo_head, _, _ = rel.partition("/")
    repo = repo_head or "__root__"
    now = int(time.time())
    metadata_json = "{}"

    row = (
        tag,
        db_path,
        start,
        end,
        snippet,
        content_hash,
        anchor_before,
        anchor_after,
        repo,
        root_id,
        note,
        commit_hash,
        now,
        now,
        metadata_json,
    )
    _enqueue_or_write(db, indexer, row)

    return {
        "tag": tag,
        "path": db_path,
        "start_line": start,
        "end_line": end,
        "hash": content_hash,
        "anchor_before": anchor_before,
        "anchor_after": anchor_after,
        "note": note,
        "commit": commit_hash,
    }


def execute_save_snippet(args: object, db: object, roots: list[str], indexer: object = None) -> ToolResult:
    """
    코드 스니펫을 태그와 함께 저장하는 도구입니다.
    파일 변경 시 위치를 복구할 수 있도록 전/후 앵커 텍스트를 함께 저장합니다.
    """
    if not isinstance(args, Mapping):
        return invalid_args_response("save_snippet", "args must be an object")

    try:
        payload = build_save_snippet(args, db, roots, indexer=indexer)
    except ValueError as e:
        msg = str(e)
        return mcp_response(
            "save_snippet",
            lambda: pack_error("save_snippet", ErrorCode.INVALID_ARGS, msg),
            lambda: {"error": {"code": ErrorCode.INVALID_ARGS.value, "message": msg}, "isError": True},
        )

    def build_pack() -> str:
        lines = [pack_header("save_snippet", {"tag": pack_encode_text(payload["tag"])}, returned=1)]
        kv = {
            "tag": pack_encode_id(payload["tag"]),
            "path": pack_encode_id(payload["path"]),
            "start": str(payload["start_line"]),
            "end": str(payload["end_line"]),
            "hash": pack_encode_id(payload["hash"]),
        }
        lines.append(pack_line("r", kv))
        return "\n".join(lines)

    return mcp_response(
        "save_snippet",
        build_pack,
        lambda: payload,
    )
