#!/usr/bin/env python
# -*- coding: utf-8 -*-
r"""
validate.py — 转换结果校验器

模拟 phpBB ACP 恢复页(acp_database.php)对 PostgreSQL 备份的解析逻辑：
  1. 按 ";\n" 切分语句（与 fgetd() 行为一致），逐条检查合法性；
  2. 识别 "COPY ... FROM stdin;" 块，按 "\n" 读取数据行直到 "\." 终止符；
  3. 将 COPY 数据按 PostgreSQL 文本格式解码，与原始 MySQL 备份中
     解码出的行值逐一比对（无损往返校验）；
  4. 检查文件名是否符合 ACP 恢复页正则、文件头尾结构、序列与 SETVAL。

用法: python validate.py <mysql备份> <转换输出>
"""

import re
import sys

import phpbb_mysql2pg as lib

FAILURES = []


def check(cond, msg):
    if cond:
        print('  [PASS] %s' % msg)
    else:
        print('  [FAIL] %s' % msg)
        FAILURES.append(msg)


# ---------------- 模拟 phpBB 恢复页解析 ----------------

def simulate_restore(text):
    """按 acp_database.php 的 fgetd(";\n") + COPY 行读取逻辑切分输出文件。"""
    events = []          # ('stmt', sql) / ('copy', header, rows)
    pos = 0
    n = len(text)
    while pos < n:
        idx = text.find(';\n', pos)
        if idx == -1:
            tail = text[pos:].strip()
            if tail:
                events.append(('stmt', tail))
            break
        stmt = text[pos:idx]
        pos = idx + 2
        t = stmt.strip()
        if not t:
            continue
        if t.startswith('COPY'):
            rows = []
            while True:
                nl = text.find('\n', pos)
                if nl == -1:
                    raise AssertionError('COPY 块缺少终止符 "\\."')
                line = text[pos:nl]
                pos = nl + 1
                if line == '\\.':
                    break
                rows.append(line)
            events.append(('copy', t, rows))
        else:
            events.append(('stmt', t))
    return events


def copy_decode_field(f):
    if f == '\\N':
        return None
    out = []
    i = 0
    n = len(f)
    while i < n:
        c = f[i]
        if c == '\\' and i + 1 < n:
            d = f[i + 1]
            m = {'n': '\n', 't': '\t', 'r': '\r', 'b': '\b',
                 'f': '\f', 'v': '\v', '\\': '\\'}
            if d in m:
                out.append(m[d])
                i += 2
                continue
            if d.isdigit():
                out.append(chr(int(f[i + 1:i + 4], 8)))
                i += 4
                continue
            out.append(d)
            i += 2
            continue
        out.append(c)
        i += 1
    return ''.join(out)


# ---------------- 从 MySQL 备份提取期望数据 ----------------

def expected_rows(text):
    """[(表名, 列名列表, 行值列表), ...]，行值为解码后的 Python 字符串或 None。"""
    out = []
    for stmt in lib.split_statements(text):
        _c, code = lib.split_comments(stmt)
        if not code:
            continue
        m = lib._INSERT_RE.match(code)
        if not m:
            continue
        table = m.group(1)
        cols = [c.strip().strip('`') for c in m.group(2).split(',')]
        out.append((table, cols, lib.parse_values(m.group(3))))
    return out


# ---------------- 校验主体 ----------------

def validate(mysql_path, output_path):

    import os
    text_in = lib.read_input(mysql_path)
    text_out = lib.read_input(output_path)

    print('== 文件级检查 ==')
    check(bool(lib.ACP_FILENAME_RE.match(os.path.basename(output_path))),
          '文件名符合 ACP 恢复页正则: %s' % os.path.basename(output_path))
    check(text_out.startswith('--\n-- phpBB Backup Script'),
          '文件头为 "--" 注释（PostgreSQL 合法）')
    check(text_out.rstrip('\n').endswith('COMMIT;'), '文件尾为 COMMIT;')
    check(text_out.startswith('--\n-- phpBB Backup Script\n') and
          'BEGIN TRANSACTION;\n' in text_out[:300], '含 BEGIN TRANSACTION;')

    print('== 语句结构检查（模拟 ACP 恢复页 fgetd 解析） ==')
    try:
        events = simulate_restore(text_out)
        ok = True
    except AssertionError as e:
        events = []
        ok = False
        print('  [FAIL] %s' % e)
        FAILURES.append(str(e))
    if ok:
        stmts = [e[1] for e in events if e[0] == 'stmt']
        copies = [e for e in events if e[0] == 'copy']
        check(all(s for s in stmts), '所有语句块非空')
        # 每条语句内不能出现裸换行/裸 \r（字符串内部），否则恢复页按 ";\n" 切分会截断
        no_raw_nl = all('\n' not in re.sub(r'--[^\n]*', '', s) or
                        not any(q and '\n' in q for q in re.findall(r"'(?:[^']|'')*'", s))
                        for s in stmts)
        check(no_raw_nl, '语句字符串字面量内无裸换行（不会被 ";\n" 切截断）')
        print('  语句数: %d；COPY 块数: %d' % (len(stmts), len(copies)))

        # COPY 块基本约束
        copy_ok = True
        for _t, header, rows in copies:
            if not re.match(r'^COPY [\w$]+ \([^)]*\) FROM stdin$', header):
                copy_ok = False
            ncols = header[header.index('(') + 1:header.rindex(')')].count(',') + 1
            for r in rows:
                if len(r.split('\t')) != ncols:
                    copy_ok = False
                if '\r' in r:
                    copy_ok = False
        check(copy_ok, '所有 COPY 数据行列数一致且无裸 \r')

        # 序列与 SETVAL（仅要求有数据行的表需要 SETVAL；空表序列从 1 开始即可）
        auto_tables = set(re.findall(r'CREATE SEQUENCE ([\w$]+)_seq', text_out))
        setvals = set(re.findall(r"SELECT SETVAL\('([\w$]+)_seq'", text_out))
        copied = {h.split()[1] for _t, h, _r in copies}
        need = {t for t in auto_tables if t in copied}
        check(need <= setvals and setvals <= auto_tables,
              '有数据的自增表均有 SETVAL（序列: %s，SETVAL: %s）'
              % (sorted(auto_tables), sorted(setvals)))

    print('== 数据无损往返校验（MySQL 解码值 == COPY 解码值） ==')
    exp = expected_rows(text_in)
    copy_iter = [e for e in events if e[0] == 'copy']
    check(len(exp) == len(copy_iter), 'INSERT 组数(%d) == COPY 块数(%d)'
          % (len(exp), len(copy_iter)))
    all_equal = True
    total = 0
    for (table, cols, rows), (_t, header, copy_rows) in zip(exp, copy_iter):
        got = []
        for r in copy_rows:
            got.append([copy_decode_field(f) for f in r.split('\t')])
        if got != rows:
            all_equal = False
            print('  [FAIL] 表 %s 数据不一致:' % table)
            for a, b in zip(got, rows):
                if a != b:
                    print('    COPY: %r' % (a,))
                    print('    MySQL: %r' % (b,))
                    break
        total += len(rows)
    check(all_equal, '全部 %d 行 / %d 个表的数据逐字段一致' %
          (total, len({t for t, _c, _r in exp})))

    print()
    if FAILURES:
        print('校验失败: %d 项' % len(FAILURES))
        return 1
    print('校验全部通过。')
    return 0


if __name__ == '__main__':
    if len(sys.argv) != 3:
        print(__doc__)
        sys.exit(2)
    sys.exit(validate(sys.argv[1], sys.argv[2]))
