#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AICESP 存量系统采集脚本（AIone Enable · 客户侧 Profiler）
═══════════════════════════════════════════════════════════

用途：在您的环境里扫描您的系统代码库，产出一份**脱敏存量画像**（结构信息，不含任何
业务数据），用于 AICESP 生成您专属的 AI 赋能方案。

我们采集什么（只有结构，没有数据）：
  · 技术栈与框架（依赖清单文件名/框架名）
  · 模块与目录结构（目录名、文件数、代码行数统计）
  · 接口清单（OpenAPI/Swagger 文件、路由定义的路径与方法）
  · 数据模型结构（表名、字段名与类型——不读任何表内数据）

我们绝不采集：
  · 任何业务数据 / 数据库内容
  · 任何密钥凭证（.env、*.pem、secrets 等文件整体跳过，绝不读取内容）
  · 任何代码文件的完整内容（只提取结构性签名）

透明承诺：本脚本无任何网络请求——产物只落在本地当前目录
（aicesp_profile_*.json + 同名 .md 供人读），由您审查后自行决定是否发送给我们
（contact@aicesp.cn）。脚本本身是可读源码，欢迎审计。

用法：
  python3 aicesp_profiler.py /path/to/your/codebase
  python3 aicesp_profiler.py .                     # 当前目录

要求：Python 3.8+，无第三方依赖。
"""
import json
import os
import re
import sys
import time

VERSION = "1.0"

# ── 安全红线：这些文件/目录整体跳过，绝不读取内容 ────────────────────────────
SECRET_FILES = {".env", ".env.local", ".env.production", "id_rsa", "id_ed25519"}
SECRET_SUFFIXES = (".pem", ".key", ".pfx", ".p12", ".keystore", ".jks")
SECRET_DIR_HINTS = ("secret", "credential", "private")
SKIP_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build",
             ".idea", ".vscode", "vendor", "target", ".next", ".nuxt", "coverage"}

CODE_EXT = {".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".go", ".php", ".rb",
            ".cs", ".vue", ".sql", ".kt", ".rs", ".c", ".cpp", ".h"}

STACK_FILES = {
    "requirements.txt": "Python (pip)", "pyproject.toml": "Python (pyproject)",
    "package.json": "Node.js", "pom.xml": "Java (Maven)", "build.gradle": "Java (Gradle)",
    "go.mod": "Go", "composer.json": "PHP (Composer)", "Gemfile": "Ruby",
    "Cargo.toml": "Rust", "*.csproj": ".NET",
    "docker-compose.yml": "Docker Compose", "Dockerfile": "Docker",
}

FRAMEWORK_HINTS = [
    ("fastapi", "FastAPI"), ("flask", "Flask"), ("django", "Django"),
    ("spring-boot", "Spring Boot"), ("springframework", "Spring"),
    ("express", "Express"), ("react", "React"), ("vue", "Vue"),
    ("angular", "Angular"), ("sqlalchemy", "SQLAlchemy"), ("mybatis", "MyBatis"),
    ("hibernate", "Hibernate"), ("gin-gonic", "Gin"), ("laravel", "Laravel"),
]

# ── 结构提取正则（只取签名/名称，绝不复制函数体）────────────────────────────
RE_ROUTES = [
    # FastAPI / Flask: @router.get("/path") / @app.route("/path", methods=[...])
    re.compile(r"@\w+\.(get|post|put|delete|patch|route)\(\s*['\"]([^'\"]+)['\"]", re.I),
    # Express: app.get('/path', ...) / router.post("/path"...
    re.compile(r"\b(?:app|router)\.(get|post|put|delete|patch)\(\s*['\"]([^'\"]+)['\"]", re.I),
    # Spring: @GetMapping("/path") 等
    re.compile(r"@(Get|Post|Put|Delete|Patch|Request)Mapping\(\s*(?:value\s*=\s*)?['\"]([^'\"]+)['\"]"),
]
RE_SQLA_TABLE = re.compile(r"__tablename__\s*=\s*['\"](\w+)['\"]")
RE_SQLA_COLUMN = re.compile(r"^\s*(\w+)\s*(?::[^=]+)?=\s*(?:mapped_column|Column)\(\s*([A-Za-z_][\w.]*)", re.M)
RE_DDL_TABLE = re.compile(r"CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`\"']?(\w+)", re.I)
RE_DJANGO_MODEL = re.compile(r"class\s+(\w+)\s*\([^)]*models\.Model")
RE_JPA_ENTITY = re.compile(r"@Entity[\s\S]{0,200}?class\s+(\w+)")


def is_secret_path(path: str, name: str) -> bool:
    low = name.lower()
    if name in SECRET_FILES or low.endswith(SECRET_SUFFIXES):
        return True
    return any(h in path.lower() for h in SECRET_DIR_HINTS)


def read_text(path: str, limit: int = 400_000) -> str:
    try:
        with open(path, "r", encoding="utf-8", errors="ignore") as f:
            return f.read(limit)
    except Exception:
        return ""


def scan(root: str) -> dict:
    profile = {
        "profiler_version": VERSION,
        "generated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
        "root_name": os.path.basename(os.path.abspath(root)) or "codebase",
        "collection_policy": "结构信息 only：技术栈/模块结构/接口路径/表与字段名。不含任何业务数据、"
                             "密钥（.env/*.pem 等整体跳过）、代码全文。产物仅落本地，无网络请求。",
        "tech_stack": [], "frameworks": [],
        "modules": [], "file_stats": {},
        "openapi_files": [], "routes": [],
        "tables": [], "skipped_secret_files": 0,
    }
    ext_count, ext_loc = {}, {}
    frameworks, routes, tables = set(), [], {}
    dir_file_count = {}

    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
        rel_dir = os.path.relpath(dirpath, root)
        for fn in filenames:
            full = os.path.join(dirpath, fn)
            rel = os.path.join(rel_dir, fn).replace("\\", "/").lstrip("./")
            if is_secret_path(rel, fn):
                profile["skipped_secret_files"] += 1
                continue
            ext = os.path.splitext(fn)[1].lower()
            # 技术栈文件
            if fn in STACK_FILES:
                item = f"{STACK_FILES[fn]} ({rel})"
                if item not in profile["tech_stack"]:
                    profile["tech_stack"].append(item)
                for hint, name in FRAMEWORK_HINTS:
                    if hint in read_text(full, 100_000).lower():
                        frameworks.add(name)
            # OpenAPI 文件
            if fn.lower() in ("openapi.json", "openapi.yaml", "openapi.yml",
                              "swagger.json", "swagger.yaml"):
                profile["openapi_files"].append(rel)
            if ext not in CODE_EXT:
                continue
            text = read_text(full)
            loc = text.count("\n") + 1 if text else 0
            ext_count[ext] = ext_count.get(ext, 0) + 1
            ext_loc[ext] = ext_loc.get(ext, 0) + loc
            top = rel.split("/")[0] if "/" in rel else "(root)"
            dir_file_count[top] = dir_file_count.get(top, 0) + 1
            # 路由（路径+方法·不带实现）
            for pat in RE_ROUTES:
                for m in pat.finditer(text):
                    routes.append({"method": m.group(1).upper(), "path": m.group(2), "file": rel})
            # 数据模型（表名/字段名+类型·不读数据）
            for m in RE_SQLA_TABLE.finditer(text):
                t = m.group(1)
                cols = [{"name": c.group(1), "type": c.group(2)}
                        for c in RE_SQLA_COLUMN.finditer(text)][:80]
                tables.setdefault(t, {"table": t, "source": rel, "columns": cols})
            for m in RE_DDL_TABLE.finditer(text):
                tables.setdefault(m.group(1), {"table": m.group(1), "source": rel, "columns": []})
            for m in RE_DJANGO_MODEL.finditer(text):
                tables.setdefault(m.group(1), {"table": m.group(1), "source": rel, "columns": [],
                                               "kind": "django_model"})
            for m in RE_JPA_ENTITY.finditer(text):
                tables.setdefault(m.group(1), {"table": m.group(1), "source": rel, "columns": [],
                                               "kind": "jpa_entity"})

    # 去重路由（method+path）
    seen = set()
    for r in routes:
        k = (r["method"], r["path"])
        if k not in seen:
            seen.add(k)
            profile["routes"].append(r)
    profile["frameworks"] = sorted(frameworks)
    profile["file_stats"] = {e: {"files": ext_count[e], "loc": ext_loc.get(e, 0)}
                             for e in sorted(ext_count)}
    profile["modules"] = [{"dir": d, "code_files": n}
                          for d, n in sorted(dir_file_count.items(), key=lambda x: -x[1])]
    profile["tables"] = sorted(tables.values(), key=lambda t: t["table"])
    return profile


def self_audit(profile: dict) -> list:
    """自审：确认产物里没有疑似密钥/长随机串（透明承诺的机器兑现）。"""
    blob = json.dumps(profile, ensure_ascii=False)
    warnings = []
    for pat, label in [
        (r"(?i)(api[_-]?key|secret|passwd|password)\s*[:=]\s*['\"][^'\"]{8,}", "疑似密钥赋值"),
        (r"\b[A-Za-z0-9+/]{40,}={0,2}\b", "疑似 base64 长串"),
        (r"\bsk-[A-Za-z0-9]{20,}\b", "疑似 API key"),
    ]:
        if re.search(pat, blob):
            warnings.append(label)
    return warnings


def write_outputs(profile: dict) -> tuple:
    ts = time.strftime("%Y%m%d_%H%M%S")
    jf = f"aicesp_profile_{ts}.json"
    mf = f"aicesp_profile_{ts}.md"
    with open(jf, "w", encoding="utf-8") as f:
        json.dump(profile, f, ensure_ascii=False, indent=1)
    lines = [
        f"# AICESP 存量系统画像 · {profile['root_name']}",
        f"\n> 生成时间 {profile['generated_at']} · 采集脚本 v{VERSION}",
        f"\n> **采集范围**：{profile['collection_policy']}",
        "\n## 技术栈", *[f"- {t}" for t in profile["tech_stack"] or ["（未识别）"]],
        "\n## 框架", "- " + ("、".join(profile["frameworks"]) or "（未识别）"),
        "\n## 模块结构（按代码文件数）",
        *[f"- {m['dir']}：{m['code_files']} 个代码文件" for m in profile["modules"][:20]],
        "\n## 接口清单", f"- OpenAPI 文件：{profile['openapi_files'] or '（未发现）'}",
        f"- 路由定义：共 {len(profile['routes'])} 条",
        *[f"  - `{r['method']} {r['path']}`" for r in profile["routes"][:60]],
        ("  - …（其余见 JSON）" if len(profile["routes"]) > 60 else ""),
        "\n## 数据模型（表/实体·只有名称与字段类型）", f"- 共 {len(profile['tables'])} 个",
        *[f"  - `{t['table']}`（{len(t.get('columns', []))} 字段）" for t in profile["tables"][:60]],
        f"\n## 隐私自审", f"- 跳过密钥类文件：{profile['skipped_secret_files']} 个（内容未读取）",
        "\n---\n**下一步**：请审阅本文件与同名 JSON，确认无敏感内容后，发送至 "
        "contact@aicesp.cn，AICESP 将为您生成专属 AI 赋能方案（每个赋能点带价值主张与人工介入设计）。",
    ]
    with open(mf, "w", encoding="utf-8") as f:
        f.write("\n".join(x for x in lines if x is not None))
    return jf, mf


def main():
    root = sys.argv[1] if len(sys.argv) > 1 else "."
    if not os.path.isdir(root):
        print(f"❌ 目录不存在: {root}")
        sys.exit(1)
    print(f"AICESP 采集脚本 v{VERSION} —— 只采结构·不采数据·产物只落本地")
    print(f"扫描: {os.path.abspath(root)} …")
    profile = scan(root)
    warnings = self_audit(profile)
    if warnings:
        print(f"⚠️ 自审提示: {warnings} —— 请人工检查产物后再发送")
    jf, mf = write_outputs(profile)
    print(f"\n✅ 完成：")
    print(f"   画像(JSON): {jf}")
    print(f"   画像(可读): {mf}")
    print(f"   路由 {len(profile['routes'])} 条 · 表/实体 {len(profile['tables'])} 个 · "
          f"跳过密钥文件 {profile['skipped_secret_files']} 个")
    print("\n请先审阅以上两个文件（全部内容一目了然），确认后发送至 contact@aicesp.cn。")
    print("本脚本未做任何网络请求。")


if __name__ == "__main__":
    main()
