#!/usr/bin/env python3
"""
인비온 DB 덤프 민감정보 마스킹 스크립트 (v2).

단계:
1) 원본 company_mng / user_info 에서 매핑 수집
   - 실제 회사명 -> '회사_{company_code}'
   - 실제 실명 -> '사용자_{sabun}'
2) INSERT 문의 대상 컬럼은 구조적 마스킹 (user_password, 연락처, 사업자번호 등)
3) 파일 전체에서 실제 회사명/실명 문자열을 전역 치환
   (메뉴 이름, 부서 company_name, 다국어 텍스트 등 어디 박혀있든)
"""

import re
from pathlib import Path

INPUT = Path("/Users/gbpark/invyone/notes/gbpark/2026-04-21-db-sample/invion-masters.sql")
OUTPUT = Path("/Users/gbpark/invyone/notes/gbpark/2026-04-21-db-sample/invion-masters.masked.sql")

INSERT_RE = re.compile(
    r"^INSERT INTO public\.(\w+) \(([^)]+)\) VALUES \((.+)\);\s*$",
    re.DOTALL,
)

# 일반명사/짧은 약자는 전역 치환하면 다른 무해한 단어를 오염시킬 수 있어 스킵
SKIP_COMPANY_NAMES = {"공통", "테스트회사", "시연용 회사"}
# 일반 역할명/호칭은 실명이 아니라 메뉴/UI 에 흔히 등장하므로 스킵
SKIP_USER_NAMES = {"관리자", "운영자", "담당자", "사용자", "개발자", "테스트"}


def parse_sql_values(s: str):
    values = []
    i, n = 0, len(s)
    while i < n:
        while i < n and s[i] in " \t\n":
            i += 1
        if i >= n:
            break
        if s[i] == ",":
            i += 1
            continue
        if s[i] == "'":
            j = i + 1
            while j < n:
                if s[j] == "'":
                    if j + 1 < n and s[j + 1] == "'":
                        j += 2
                    else:
                        break
                else:
                    j += 1
            values.append(s[i:j + 1])
            i = j + 1
        elif s[i:i + 4].upper() == "NULL" and (i + 4 == n or not s[i + 4].isalnum()):
            values.append("NULL")
            i += 4
        elif s[i:i + 4].upper() == "TRUE" and (i + 4 == n or not s[i + 4].isalnum()):
            values.append("TRUE")
            i += 4
        elif s[i:i + 5].upper() == "FALSE" and (i + 5 == n or not s[i + 5].isalnum()):
            values.append("FALSE")
            i += 5
        elif s[i] == "-" or s[i].isdigit():
            j = i + 1
            while j < n and (s[j].isdigit() or s[j] in ".-eE+"):
                j += 1
            values.append(s[i:j])
            i = j
        else:
            j = i
            depth = 0
            while j < n and (s[j] != "," or depth > 0):
                if s[j] == "(":
                    depth += 1
                elif s[j] == ")":
                    depth -= 1
                j += 1
            values.append(s[i:j].strip())
            i = j
    return values


def strip_q(v: str) -> str:
    if v.startswith("'") and v.endswith("'"):
        return v[1:-1].replace("''", "'")
    return v


def collect_mappings(src: str):
    company_map: dict[str, str] = {}
    user_map: dict[str, str] = {}
    for line in src.splitlines():
        m = INSERT_RE.match(line + "\n")
        if not m:
            continue
        tbl, cols_s, vals_s = m.groups()
        if tbl not in ("company_mng", "user_info"):
            continue
        cols = [c.strip() for c in cols_s.split(",")]
        vals = parse_sql_values(vals_s)
        if len(cols) != len(vals):
            continue
        row = dict(zip(cols, vals))
        if tbl == "company_mng":
            code = strip_q(row.get("company_code", ""))
            name = strip_q(row.get("company_name", ""))
            if name and name not in SKIP_COMPANY_NAMES and len(name) >= 2:
                masked = f"회사_{code}"
                # 등록명 그대로
                company_map[name] = masked
                # 법인 접두/접미 제거 alias (짧은 이름으로만 쓰이는 경우 대응)
                for stripped in (
                    name.removeprefix("(주)").strip(),
                    name.removeprefix("주식회사 ").strip(),
                    name.removesuffix(" 주식회사").strip(),
                    name.removesuffix("주식회사").strip(),
                ):
                    if stripped and stripped != name and stripped not in SKIP_COMPANY_NAMES and len(stripped) >= 2:
                        company_map[stripped] = masked
        elif tbl == "user_info":
            sabun = strip_q(row.get("sabun", ""))
            name = strip_q(row.get("user_name", ""))
            if name and name not in SKIP_USER_NAMES and len(name) >= 2:
                user_map[name] = f"사용자_{sabun}"
    return company_map, user_map


def mask_line_columns(line: str) -> str:
    m = INSERT_RE.match(line)
    if not m:
        return line
    tbl, cols_s, vals_s = m.groups()
    cols = [c.strip() for c in cols_s.split(",")]
    vals = parse_sql_values(vals_s)
    if len(cols) != len(vals):
        return line
    row = dict(zip(cols, vals))

    if tbl == "user_info":
        if "user_password" in row:
            row["user_password"] = "'REDACTED'"
        if "sabun" in row and "user_name" in row:
            sabun = strip_q(row["sabun"]) or "UNKNOWN"
            row["user_name"] = f"'사용자_{sabun}'"
        if "user_name_eng" in row:
            row["user_name_eng"] = "''"
        if "user_name_cn" in row:
            row["user_name_cn"] = "''"
        for f in ("email", "tel", "cell_phone", "fax_no"):
            if f in row:
                row[f] = "NULL"

    elif tbl == "company_mng":
        if "company_code" in row and "company_name" in row:
            code = strip_q(row["company_code"]) or "UNKNOWN"
            row["company_name"] = f"'회사_{code}'"
        for f in (
            "business_registration_number",
            "representative_name",
            "representative_phone",
            "email",
            "website",
            "address",
            "company_image",
            "company_logo",
            "company_seal",
        ):
            if f in row:
                row[f] = "NULL"

    elif tbl == "dept_info":
        # 부서 자체 이름은 유지 (일반 부서명), 회사명 컬럼은 전역 치환이 처리
        pass

    new_vals = [row[c] for c in cols]
    return f"INSERT INTO public.{tbl} ({', '.join(cols)}) VALUES ({', '.join(new_vals)});\n"


def apply_global_replacements(text: str, company_map: dict, user_map: dict):
    # 긴 문자열부터 치환 (부분 중복 방지: "(주)제일그라스" 먼저, "제일그라스" 나중)
    items = sorted(
        list(company_map.items()) + list(user_map.items()),
        key=lambda kv: -len(kv[0]),
    )
    replacements = 0
    for original, masked in items:
        before = text.count(original)
        if before:
            text = text.replace(original, masked)
            replacements += before
    return text, replacements


def main():
    src = INPUT.read_text(encoding="utf-8")
    company_map, user_map = collect_mappings(src)

    print(f"수집된 회사 매핑 {len(company_map)}건:")
    for k, v in company_map.items():
        print(f"  '{k}' -> '{v}'")
    print(f"수집된 실명 매핑 {len(user_map)}건:")
    for k, v in user_map.items():
        print(f"  '{k}' -> '{v}'")

    # 1) 컬럼 단위 마스킹
    col_masked_lines = []
    col_counts = {"user_info": 0, "company_mng": 0}
    for line in src.splitlines(keepends=True):
        if line.startswith("INSERT INTO public."):
            m = INSERT_RE.match(line)
            if m:
                tbl = m.group(1)
                new_line = mask_line_columns(line)
                if tbl in col_counts and new_line != line:
                    col_counts[tbl] += 1
                col_masked_lines.append(new_line)
                continue
        col_masked_lines.append(line)

    stage1_text = "".join(col_masked_lines)

    # 2) 전역 문자열 치환 (나머지 위치의 회사명/실명)
    stage2_text, global_replacements = apply_global_replacements(
        stage1_text, company_map, user_map
    )

    OUTPUT.write_text(stage2_text, encoding="utf-8")

    print(f"\n완료: {OUTPUT}")
    print(f"  컬럼 단위 마스킹 - user_info: {col_counts['user_info']}건")
    print(f"  컬럼 단위 마스킹 - company_mng: {col_counts['company_mng']}건")
    print(f"  전역 문자열 치환: {global_replacements}건 (회사명/실명)")


if __name__ == "__main__":
    main()
