"""Build a two-sheet human checklist from agent-curated, source-mapped JSON."""
import argparse
import json
import math
from pathlib import Path

LAYOUT = {
    'procurement': ('采购内容总览', ('category', 'item', 'summary', 'action'),
                    ('类别', '采购内容（用途）', '核心要求', '需准备 / 确认'), (14, 34, 59, 48)),
    'commercial': ('商务履约清单', ('topic', 'agreement', 'numbers', 'action'),
                   ('事项', '核心约定', '关键数字', '需准备 / 确认'), (18, 55, 23, 55)),
}


def validate(data):
    if not isinstance(data, dict):
        raise ValueError('input must be an object')
    if not isinstance(data.get('title'), str) or not data['title'].strip():
        raise ValueError('title is required')
    sources = data.get('sources', [])
    if not isinstance(sources, list) or not sources:
        raise ValueError('sources are required')
    source_ids = set()
    for source in sources:
        sid = source.get('id')
        if not isinstance(sid, str) or not sid.strip() or sid in source_ids or not source.get('name'):
            raise ValueError('invalid or duplicate source id/name')
        source_ids.add(sid)
    requirements = data.get('requirements', [])
    if not isinstance(requirements, list) or not requirements:
        raise ValueError('requirements are required')
    reqs = {}
    for req in requirements:
        rid = req.get('id')
        if not isinstance(rid, str) or not rid.strip() or rid in reqs:
            raise ValueError('invalid or duplicate requirement id')
        if req.get('source_id') not in source_ids or not req.get('locator'):
            raise ValueError(f'{rid}: unknown source or missing locator')
        if not isinstance(req.get('critical'), bool):
            raise ValueError(f'{rid}: critical must be boolean')
        tokens = req.get('must_keep', [])
        if not isinstance(tokens, list) or any(not isinstance(t, str) or not t.strip() for t in tokens):
            raise ValueError(f'{rid}: invalid must_keep')
        if req['critical'] and not tokens:
            raise ValueError(f'{rid}: critical requirement needs explicit preserved terms')
        reqs[rid] = req
    mapped = {rid: [] for rid in reqs}
    warnings = []
    for key, (title, fields, _, _) in LAYOUT.items():
        rows = data.get(key)
        if not isinstance(rows, list) or not rows:
            raise ValueError(f'{key}: nonempty list required')
        seen = set()
        for index, row in enumerate(rows, 1):
            if any(not isinstance(row.get(f), str) for f in fields):
                raise ValueError(f'{key} row {index}: visible fields must be text')
            if not row[fields[0]].strip() or not row[fields[1]].strip():
                raise ValueError(f'{key} row {index}: empty item')
            identity = tuple(row[f].strip() for f in fields)
            if identity in seen:
                raise ValueError(f'{key}: duplicate visible row')
            seen.add(identity)
            refs = row.get('refs')
            if not isinstance(refs, list) or not refs or any(not isinstance(r, str) or r not in reqs for r in refs):
                raise ValueError(f'{key} row {index}: missing or unknown refs')
            text = '\n'.join(row[f] for f in fields)
            for rid in set(refs):
                mapped[rid].append(text)
            if any(len(row[f]) > 80 for f in fields):
                warnings.append(f'{title} row {index}: long cell; review before publication')
    for rid, req in reqs.items():
        if not mapped[rid]:
            raise ValueError(f'{rid}: requirement omitted from summary')
        joined = '\n'.join(mapped[rid])
        for token in req.get('must_keep', []):
            if token not in joined:
                raise ValueError(f'{rid}: preserved term missing: {token}')
    return {'source_count': len(sources), 'requirement_count': len(reqs),
            'critical_count': sum(r['critical'] for r in reqs.values()),
            'sheets': {v[0]: len(data[k]) for k, v in LAYOUT.items()},
            'warnings': warnings, 'coverage': 'all mapped; semantic review still required'}


def build(data, output):
    report = validate(data)
    output = Path(output)
    trace = output.with_suffix('.trace.json')
    if output.suffix.lower() != '.xlsx':
        raise ValueError('output must end in .xlsx')
    if output.exists() or trace.exists():
        raise ValueError('output or trace exists; choose a new version path')
    from openpyxl import Workbook, load_workbook
    from openpyxl.styles import Alignment, Font, PatternFill
    from openpyxl.utils import get_column_letter
    wb = Workbook()
    wb.remove(wb.active)
    for key, (title, fields, headers, widths) in LAYOUT.items():
        ws = wb.create_sheet(title)
        for rno, values in enumerate([headers] + [[r[f] for f in fields] for r in data[key]], 1):
            lines = 1
            for cno, (text, width) in enumerate(zip(values, widths), 1):
                cell = ws.cell(rno, cno, text)
                cell.data_type = 's'  # User text beginning with '=' stays text.
                cell.font = Font(name='微软雅黑', size=12, bold=rno == 1,
                                 color='FFFFFF' if rno == 1 else '263445')
                cell.alignment = Alignment(vertical='top', wrap_text=True)
                cell.fill = PatternFill('solid', fgColor='174B6B' if rno == 1 else ('F1F6FA' if rno % 2 == 0 else 'FFFFFF'))
                estimate = sum(max(1, math.ceil(sum(2 if ord(c) > 127 else 1 for c in line) / (width - 2))) for line in text.split('\n'))
                lines = max(lines, estimate)
                ws.column_dimensions[get_column_letter(cno)].width = width
            ws.row_dimensions[rno].height = 32 if rno == 1 else min(409, max(54, 20 * lines + 10))
        ws.freeze_panes = 'C2'
        ws.auto_filter.ref = ws.dimensions
    output.parent.mkdir(parents=True, exist_ok=True)
    wb.save(output)
    check = load_workbook(output)
    for key, (title, fields, headers, _) in LAYOUT.items():
        assert check[title].max_column == 4 and check[title].max_row == len(data[key]) + 1
        expected = [list(headers)] + [[r[f] for f in fields] for r in data[key]]
        actual = [[c.value or '' for c in row] for row in check[title]]
        assert actual == expected, title
        assert all(c.data_type not in ('f', 'e') for row in check[title] for c in row)
    trace.write_text(json.dumps({'report': report, 'sources': data['sources'],
                                 'requirements': data['requirements'],
                                 'row_mapping': {k: [r['refs'] for r in data[k]] for k in LAYOUT}}, ensure_ascii=False, indent=2))
    return report


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--input', required=True)
    parser.add_argument('--output')
    parser.add_argument('--validate-only', action='store_true')
    args = parser.parse_args()
    data = json.loads(Path(args.input).read_text())
    try:
        if args.validate_only:
            report = validate(data)
        else:
            if not args.output:
                parser.error('--output is required unless --validate-only')
            report = build(data, args.output)
        print(json.dumps(report, ensure_ascii=False, indent=2))
    except ValueError as exc:
        parser.exit(1, f'{exc}\n')


if __name__ == '__main__':
    main()
