# 写真のファイル名を「撮影日_連番」（例: 2026-05-03_001.jpg）にまとめて変えるスクリプト
# 作成: AI（Claude）／Alice in Sandbox のレシピ用サンプル
#
# 使い方（このファイルと同じ場所で、ターミナルに打つ）:
#   python rename_photos.py photos          … 試し運転。「元の名前 → 新しい名前」の一覧を出すだけ
#   python rename_photos.py photos --run    … 名前を変える。先に対応表 rename_log.csv を書き出す
#   python rename_photos.py photos --undo   … rename_log.csv を使って元の名前に戻す
#
# 撮影日は写真の EXIF（DateTimeOriginal）から読み、無ければファイルの更新日時を使う。
# 名前を変えるだけで、写真の中身（位置情報などの EXIF を含む）は書き換えない。
# 必要なもの: Python 3.10 以降と Pillow（pip install pillow）
import csv
import sys
from datetime import datetime
from pathlib import Path

from PIL import Image

EXTS = {".jpg", ".jpeg", ".png", ".heic"}  # 対象にする拡張子（大文字・小文字は区別しない）
LOG_NAME = "rename_log.csv"                 # 元の名前と新しい名前の対応表
EXIF_IFD = 0x8769                           # 撮影日が入っている EXIF の区画
DATETIME_ORIGINAL = 36867                   # 撮影日時（DateTimeOriginal）の番号
TEMP = "__renaming__"                       # 付け替えの途中だけ使う仮の名前の頭
STATE = TEMP + "state.txt"                  # 付け替えがどこまで進んだかのメモ（終われば消える）
LOG_EXISTS = (f"{LOG_NAME} が既にあります。前回の分を --undo で戻すか、"
              "対応表を別の場所に移してから実行してください（上書きすると元に戻せなくなります）")


def shot_date(path):
    """撮影日時と、その出どころ（"EXIF" か "更新日時"）を返す"""
    try:
        with Image.open(path) as img:
            value = img.getexif().get_ifd(EXIF_IFD).get(DATETIME_ORIGINAL)
        if value:
            return datetime.strptime(str(value).strip("\x00 "), "%Y:%m:%d %H:%M:%S"), "EXIF"
    except (OSError, ValueError):
        pass  # 開けない形式（HEIC など）や、日付が空・壊れているときは更新日時へ
    return datetime.fromtimestamp(path.stat().st_mtime), "更新日時"


def make_plan(folder):
    """名前の付け替え表を作る（ファイルにはまだ触らない）"""
    photos = []
    for path in folder.iterdir():
        if path.is_file() and path.suffix.lower() in EXTS and not path.name.startswith(TEMP):
            date, source = shot_date(path)
            photos.append((date, path.name, source))
    photos.sort()  # 撮影日時の順。同じ日時なら元の名前の順

    counter = {}
    plan = []
    for date, name, source in photos:
        day = date.strftime("%Y-%m-%d")
        counter[day] = counter.get(day, 0) + 1
        new = f"{day}_{counter[day]:03d}{Path(name).suffix.lower()}"
        plan.append({"old": name, "new": new,
                     "date": date.strftime("%Y-%m-%d %H:%M:%S"), "source": source})
    return plan


def show(plan):
    for row in plan:
        mark = "（変更なし）" if row["old"] == row["new"] else ""
        print(f"{row['old']} → {row['new']}  [{row['source']} {row['date']}]{mark}")


def move_all(folder, pairs, kind):
    """[(今の名前, 新しい名前), ...] を、名前がぶつからないように2段階で付け替える。
    どこまで進んだかを STATE に書いておき、途中で止まっても --undo で続きから直せるようにする"""
    state = folder / STATE
    pairs = [(a, b) for a, b in pairs if a != b]
    state.write_text(f"{kind} 1", encoding="utf-8")
    for a, b in pairs:   # 1段目: いったん仮の名前（__renaming__新しい名前）へ
        (folder / a).rename(folder / (TEMP + b))
    state.write_text(f"{kind} 2", encoding="utf-8")
    for _, b in pairs:   # 2段目: 仮の名前から新しい名前へ
        (folder / (TEMP + b)).rename(folder / b)
    return len(pairs)


def finish(folder, pairs, phase):
    """途中で止まった付け替えを、最後まで済ませる"""
    pairs = [(a, b) for a, b in pairs if a != b]
    if phase == "1":
        for a, b in pairs:
            if not (folder / (TEMP + b)).exists() and (folder / a).exists():
                (folder / a).rename(folder / (TEMP + b))
    for _, b in pairs:
        if (folder / (TEMP + b)).exists():
            (folder / (TEMP + b)).rename(folder / b)


def read_log(log):
    with open(log, encoding="utf-8-sig", newline="") as f:
        return list(csv.DictReader(f))


def run(folder, plan):
    log = folder / LOG_NAME
    if log.exists():
        sys.exit(LOG_EXISTS)
    if any(p.name.startswith(TEMP) for p in folder.iterdir()):
        sys.exit(f"{TEMP} で始まるファイルがあります。前回が途中で止まっています。先に --undo で戻してください")
    olds = {r["old"].lower() for r in plan}
    for r in plan:  # 名前を変えない物（同じ名前のフォルダなど）とぶつからないか
        if (folder / r["new"]).exists() and r["new"].lower() not in olds:
            sys.exit(f"{r['new']} という名前のものが既にあるので止めました（何も変えていません）")
    # 先に対応表を書く。途中で止まっても、この表で戻せる
    with open(log, "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["old", "new", "date", "source"])
        writer.writeheader()
        writer.writerows(plan)
    count = move_all(folder, [(r["old"], r["new"]) for r in plan], "run")
    (folder / STATE).unlink()
    print(f"{count}件の名前を変えました。対応表: {LOG_NAME}")


def undo(folder):
    log = folder / LOG_NAME
    state = folder / STATE
    if not log.exists():
        sys.exit(f"{LOG_NAME} が見つかりません。戻せるのは --run で名前を変えたフォルダだけです")
    rows = read_log(log)
    run_pairs = [(r["old"], r["new"]) for r in rows]
    undo_pairs = [(r["new"], r["old"]) for r in rows]

    kind = None
    if state.exists():  # 前回の --run か --undo が途中で止まっている
        kind, phase = state.read_text(encoding="utf-8").split()
        finish(folder, run_pairs if kind == "run" else undo_pairs, phase)
        print(f"途中で止まっていた付け替え（--{kind}）を最後まで済ませました")

    count = 0
    if kind != "undo":
        pairs = []
        for now, old in undo_pairs:
            if (folder / now).exists():
                pairs.append((now, old))
            else:
                print(f"見つからないので飛ばします: {now}")
        count = move_all(folder, pairs, "undo")
    state.write_text("undo 3", encoding="utf-8")  # 3 = あとは対応表の名前を変えるだけ
    done = folder / "rename_log_undone.csv"
    n = 2
    while done.exists():
        done = folder / f"rename_log_undone_{n}.csv"
        n += 1
    log.rename(done)  # 戻し終わった対応表は名前を変えて残す（消さない）
    state.unlink()
    print(f"{count}件を元の名前に戻しました。対応表は {done.name} に名前を変えて残しました")


def main(argv):
    if not argv:
        sys.exit("使い方: python rename_photos.py フォルダ名 [--run | --undo]")
    folder = Path(argv[0])
    if not folder.is_dir():
        sys.exit(f"フォルダが見つかりません: {folder}")
    mode = argv[1] if len(argv) > 1 else ""
    if mode not in ("", "--run", "--undo"):
        sys.exit(f"{mode} は使えません。--run か --undo を付けるか、何も付けずに実行してください")
    if mode == "--undo":
        undo(folder)
        return
    if mode == "--run" and (folder / LOG_NAME).exists():
        sys.exit(LOG_EXISTS)
    plan = make_plan(folder)
    show(plan)
    if mode == "--run":
        run(folder, plan)
    else:
        print(f"試し運転です（{len(plan)}件・名前は変えていません）。よければ --run を付けて実行してください")


if __name__ == "__main__":
    main(sys.argv[1:])
