"""記事C 演習の共通安全確認（rev2、GPT再レビュー R2 対応）

drill.py と cleanup_v1_0/1_1.py が同じ検査を使うための小さなモジュール。
前提は「単独実行・架空データ・通常のローカルファイル」。コードや目印を故意に書き換える利用者、
別プロセスによる差替え、OSのあらゆる特殊ファイルへの防御は対象にしていない。

決めごと:
  - 信頼する基点（BASE）は、このファイルがあるフォルダ（examples/sample-c）だけ。
  - 演習領域（WORK）は BASE の直下にあり、名前が work で始まるフォルダだけ。
    環境変数 DRILL_WORK_DIR で指定できるが、この条件を満たさなければ拒否する。
  - 演習領域は、目印ファイル .drill-work が「内容つき」で一致するときだけ初期化済みとみなす。
    目印は JSON で、種別と演習領域の実パスを持つ。空の目印や別領域の目印は無効。
  - 演習領域以下のシンボリックリンクは、読取り元・書込み先・削除先のいずれでも拒否する。
    リンク先がまだ存在しないリンクも「既にある保存先」として拒否する。
  - macOS のシステム側のパス正規化（/var → /private/var など）は realpath で両側を揃えて扱い、
    演習領域以下のリンクとは区別する。
"""
import json
import os
from datetime import datetime, timezone

BASE = os.path.realpath(os.path.dirname(os.path.abspath(__file__)))
MARKER_NAME = ".drill-work"
MARKER_KIND = "ai-system-development/sample-c drill work area"


class SafetyError(Exception):
    """安全確認に失敗した。呼出し側は何も変更せずに終了する。"""


def resolve_work():
    """演習領域の絶対パスを決める。基点直下・名前・リンクでないことを確かめる。"""
    raw = os.environ.get("DRILL_WORK_DIR") or os.path.join(BASE, "work")
    work = os.path.abspath(raw)
    if os.path.islink(work):
        raise SafetyError(f"演習領域がシンボリックリンクである: {work}")
    if os.path.realpath(os.path.dirname(work)) != BASE:
        raise SafetyError(f"演習領域は {BASE} の直下でなければならない: {work}")
    if not os.path.basename(work).startswith("work"):
        raise SafetyError(f"演習領域の名前は work で始まる必要がある: {work}")
    return os.path.join(BASE, os.path.basename(work))


def marker_path(work):
    return os.path.join(work, MARKER_NAME)


def write_marker(work):
    data = {"kind": MARKER_KIND, "work_realpath": os.path.realpath(work),
            "created_at": datetime.now(timezone.utc).isoformat(timespec="seconds")}
    with open(marker_path(work), "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False)


def marker_valid(work):
    """目印が存在し、リンクでなく、内容（種別・実パス）がこの演習領域と一致するか。"""
    m = marker_path(work)
    if not os.path.lexists(m) or os.path.islink(m) or not os.path.isfile(m):
        return False
    try:
        with open(m, encoding="utf-8") as f:
            data = json.load(f)
    except (OSError, ValueError):
        return False   # 空の旧形式の目印もここで無効になる
    return (isinstance(data, dict) and data.get("kind") == MARKER_KIND
            and data.get("work_realpath") == os.path.realpath(work))


def require_initialized(work):
    if not os.path.isdir(work) or os.path.islink(work):
        raise SafetyError(f"演習領域がない、またはフォルダでない: {work}")
    if not marker_valid(work):
        raise SafetyError(f"演習領域の目印が無いか内容が一致しない（初期化済みと確認できない）: {work}")


def check_inside(work, path, what, must_exist=None, allow_dir=False):
    """path が演習領域の中の通常のファイル／フォルダであることを最終要素まで確かめる。

    must_exist: True=存在必須（通常のファイルかフォルダ）、False=存在してはならない（リンク含む）、None=どちらでも。
    途中の要素にリンクがあれば拒否。実パスが演習領域の外なら拒否。
    """
    path = os.path.abspath(path)
    root = os.path.realpath(work)
    real = os.path.realpath(path)
    if not (real == root or real.startswith(root + os.sep)):
        raise SafetyError(f"{what} の実際の場所が演習領域の外にある: {path} → {real}")
    # 演習領域から path までの各要素にリンクがないか
    rel = os.path.relpath(path, work)
    cur = work
    for part in rel.split(os.sep):
        if part in ("", "."):
            continue
        if part == "..":
            raise SafetyError(f"{what} が演習領域の外を指している: {path}")
        cur = os.path.join(cur, part)
        if os.path.islink(cur):
            raise SafetyError(f"{what} の経路にシンボリックリンクがある: {cur}")
    exists = os.path.lexists(path)
    if must_exist is True and not exists:
        raise SafetyError(f"{what} が存在しない: {path}")
    if must_exist is False and exists:
        raise SafetyError(f"{what} が既に存在する（リンクや空ファイルを含む）。上書きしない: {path}")
    if exists:
        if os.path.isdir(path) and not allow_dir:
            raise SafetyError(f"{what} がフォルダである: {path}")
        if not os.path.isdir(path) and not os.path.isfile(path):
            raise SafetyError(f"{what} が通常のファイルでない: {path}")
    return path


def check_dir_inside(work, path, what):
    """演習領域内のフォルダ（作成予定を含む）。存在すれば通常のフォルダであること。"""
    return check_inside(work, path, what, must_exist=None, allow_dir=True)
