#!/usr/bin/env python3
"""列文件名小工具。在每台存图纸的电脑上跑一次，产出“图纸文件清单”csv。

只读：用 os.scandir 取名字、修改时间、大小，不打开任何文件。只用标准库，不联网。
输出的路径相对扫描根，前面加根文件夹的名字（示例图纸库/客户甲/QM-51325.dwg），不含盘符和根之上的路径。

用法：
  python listfiles.py --root D:\\示例图纸库 [--root E:\\老图纸库] --computer 设计机 --out 清单.csv [--ext dwg,dxf,stp,step,pdf,prt]

已知限制：Windows 超长路径（260 字符以上）不专门处理，读不出的条目跳过并计入“读失败”。
"""
import argparse
import csv
import ntpath
import os
import posixpath
import socket
import sys
import time
from datetime import datetime

DEFAULT_EXTS = frozenset({".dwg", ".dxf", ".stp", ".step", ".pdf", ".prt"})
HEADER = ["电脑", "路径", "修改时间", "大小"]


def parse_exts(text):
    out = set()
    for part in str(text).split(","):
        part = part.strip().lower()
        if part:
            out.add(part if part.startswith(".") else "." + part)
    return frozenset(out)


def _parts(pathmod, p):
    drive, rest = pathmod.splitdrive(p)
    segs = [s for s in rest.replace("\\", "/").split("/") if s] if pathmod is ntpath else \
        [s for s in rest.split("/") if s]
    return drive, segs


def relative_path(root, full, pathmod=None):
    """full 相对 root 的写法：根文件夹名 + 层级，分隔符 /，不含盘符。full 不在 root 下抛 ValueError。
    pathmod 可传 ntpath 在 macOS 上测 Windows 写法。"""
    pathmod = pathmod or os.path
    key = pathmod.normcase
    rd, rsegs = _parts(pathmod, root)
    fd, fsegs = _parts(pathmod, full)
    if key(rd) != key(fd):
        raise ValueError(f"不在扫描根下：{full}")
    if len(fsegs) <= len(rsegs) or [key(s) for s in fsegs[:len(rsegs)]] != [key(s) for s in rsegs]:
        raise ValueError(f"不在扫描根下：{full}")
    rel = fsegs[len(rsegs):]
    name = rsegs[-1] if rsegs else ""
    return "/".join(([name] if name else []) + rel)


def _walk(root, exts, stats, out):
    stack = [root]
    while stack:
        d = stack.pop()
        try:
            it = os.scandir(d)
        except PermissionError:
            stats["denied"] += 1
            continue
        except OSError:
            stats["failed"] += 1
            continue
        with it:
            while True:
                try:
                    e = next(it)
                except StopIteration:
                    break
                except OSError:
                    stats["failed"] += 1
                    break
                try:
                    if e.is_symlink():
                        stats["links"] += 1
                    elif e.is_dir(follow_symlinks=False):
                        stack.append(e.path)
                    elif e.is_file(follow_symlinks=False) and os.path.splitext(e.name)[1].lower() in exts:
                        st = e.stat(follow_symlinks=False)
                        out.append((e.path, st.st_mtime, st.st_size))
                except OSError:
                    stats["failed"] += 1


def list_files(roots, computer, exts=DEFAULT_EXTS, stats=None):
    """扫描 roots，返回 [电脑, 相对路径, 'YYYY-MM-DD HH:MM', 字节数] 行列表（按路径排序）。
    stats（dict，可选）被填入 denied（没有权限的目录）、links（符号链接）、failed（其他读失败）。"""
    if stats is None:
        stats = {}
    stats.update(denied=0, links=0, failed=0)
    exts = frozenset(x.lower() for x in exts)
    rows = []
    for root in roots:
        root = os.path.abspath(root)
        found = []
        _walk(root, exts, stats, found)
        for path, mtime, size in found:
            try:
                rel = relative_path(root, path)
            except ValueError:
                stats["failed"] += 1
                continue
            try:
                stamp = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M")
            except (OSError, OverflowError, ValueError):      # 修改时间读不成日期（例如早于 1970 年）：这个文件计入读失败，扫描照常往下走
                stats["failed"] += 1
                continue
            rows.append([computer, rel, stamp, size])
    rows.sort(key=lambda r: r[1])
    return rows


def write_csv(path, rows):
    with open(path, "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(HEADER)
        w.writerows(rows)


def summary_text(n, stats, seconds):
    s = f"共列出 {n} 个图纸文件；跳过 {stats['denied']} 个目录（没有权限）、{stats['links']} 个链接；用时 {seconds:.1f} 秒"
    if stats.get("failed"):
        s += f"。另有 {stats['failed']} 个条目读失败，已跳过"
    return s


def main(argv=None):
    ap = argparse.ArgumentParser(description="列出图纸文件名（只读，不打开文件）")
    ap.add_argument("--root", action="append", required=True, help="要扫描的文件夹，可重复")
    ap.add_argument("--computer", help="这台电脑的叫法")
    ap.add_argument("--out", required=True, help="输出 csv 路径")
    ap.add_argument("--ext", default=",".join(sorted(x[1:] for x in DEFAULT_EXTS)), help="扩展名，逗号分隔")
    a = ap.parse_args(argv)
    for r in a.root:
        if not os.path.isdir(r):
            print(f"找不到文件夹：{r}", file=sys.stderr)
            return 2
    computer = a.computer
    if not computer:
        computer = socket.gethostname()
        print(f"没有给 --computer，先用“{computer}”。请用你们厂里对这台电脑的叫法，重新运行时加 --computer。")
    t0 = time.time()
    stats = {}
    rows = list_files(a.root, computer, parse_exts(a.ext), stats=stats)
    write_csv(a.out, rows)
    text = summary_text(len(rows), stats, time.time() - t0)
    print(text)
    base = os.path.splitext(a.out)[0]
    with open(base + ".summary.txt", "w", encoding="utf-8") as f:
        f.write(text + "\n")
    return 0


if __name__ == "__main__":
    sys.exit(main())
