#!/usr/bin/env python3
from __future__ import annotations

import csv
import hashlib
import json
import py_compile
import re
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
ERRORS: list[str] = []
WARNINGS: list[str] = []
PASSES: list[str] = []


def ok(message: str) -> None: PASSES.append(message)
def fail(message: str) -> None: ERRORS.append(message)
def warn(message: str) -> None: WARNINGS.append(message)


def active_files(pattern: str):
    for path in ROOT.rglob(pattern):
        rel = path.relative_to(ROOT)
        if rel.parts[:1] == ('attachments',):
            continue
        if rel.parts[:2] == ('research', 'original'):
            continue
        if any(part in {'bin', 'obj', 'node_modules', '__pycache__', '.git'} for part in rel.parts):
            continue
        yield path

required = [
    'README.md', 'RELEASE_NOTES_V12_5.md', 'SOURCE_BASIS_V12_5.md',
    'docs/v12_5/00_EXECUTIVE_CRITICAL_REVIEW.md',
    'docs/v12_5/01_SOURCE_COVERAGE_MATRIX.csv',
    'docs/v12_5/02_ROLE_BASED_BRUTAL_AUDIT.md',
    'docs/v12_5/03_TARGET_ARCHITECTURE_SOLID_REFACTORING.md',
    'docs/v12_5/04_ALGORITHM_MODEL_EVALUATION.md',
    'docs/v12_5/05_DATA_DB_INTEGRITY.md',
    'docs/v12_5/06_BE_FE_HANGFIRE_IMPLEMENTATION.md',
    'docs/v12_5/07_QA_SECURITY_OPERATIONS.md',
    'docs/v12_5/08_PROCESS_SIMPLIFICATION_VIBE_CODING.md',
    'docs/v12_5/09_ROADMAP_20D_90D_36M.md',
    'docs/v12_5/10_DETAILED_WBS_MASTER.csv',
    'docs/v12_5/11_TECH_DEBT_REGISTER.csv',
    'docs/v12_5/12_TRACEABILITY_MATRIX.csv',
    'docs/v12_5/13_DECISION_LOG.csv',
    'docs/v12_5/14_PACKAGE_CONTENTS.md',
    'docs/v12_5/15_JOB_CATALOGUE.csv',
    'docs/v12_5/16_RACI_MATRIX.csv',
    'docs/v12_5/17_RISK_REGISTER.csv',
    'docs/v12_5/18_VALIDATION_MATRIX.csv',
    'docs/v12_5/K-ArtSell_Aegis_v12_5_실행보증_통합고도화_제안서.docx',
    'db/migrations/0017_execution_assurance.sql',
    'contracts/schedules/execution-assurance.v1.json',
    'contracts/model-governance/evidence-classification.v1.json',
    'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecutionBoundary.cs',
    'tests/KArtSell.ModelOperations.UnitTests/ModelOperationExecutionBoundaryTests.cs',
]
for name in required:
    if not (ROOT / name).exists(): fail(f'missing required file: {name}')
if not ERRORS: ok(f'required v12.5 files present: {len(required)}')

# Current request direct attachment preservation.
coverage_path = ROOT / 'docs/v12_5/01_SOURCE_COVERAGE_MATRIX.csv'
coverage: dict[str, str] = {}
if coverage_path.exists():
    with coverage_path.open(encoding='utf-8-sig', newline='') as f:
        for row in csv.DictReader(f): coverage[row['Attachment']] = row['SHA256']
attachment_dir = ROOT / 'attachments/current_request_20260802'
attachments = [p for p in sorted(attachment_dir.iterdir()) if p.is_file()]
if len(attachments) != 3:
    fail(f'expected exactly three direct current-request attachments, found {len(attachments)}')
for path in attachments:
    actual = hashlib.sha256(path.read_bytes()).hexdigest()
    if coverage.get(path.name) != actual: fail(f'attachment coverage/hash mismatch: {path.name}')
if len(coverage) != len(attachments): fail('source coverage row count mismatch')
if not ERRORS: ok('three direct attachments preserved byte-for-byte')

# Parse structured files.
for path in active_files('*.json'):
    try: json.loads(path.read_text(encoding='utf-8'))
    except Exception as exc: fail(f'invalid JSON {path.relative_to(ROOT)}: {exc}')
ok('JSON parse complete')

for path in [*active_files('*.csproj'), *active_files('*.props')]:
    try: ET.parse(path)
    except Exception as exc: fail(f'invalid MSBuild XML {path.relative_to(ROOT)}: {exc}')
ok('MSBuild XML parse complete')

for path in active_files('*.csv'):
    try:
        with path.open(encoding='utf-8-sig', newline='') as f: rows = list(csv.reader(f))
        if not rows: fail(f'empty CSV {path.relative_to(ROOT)}'); continue
        width = len(rows[0])
        bad = [i + 1 for i, row in enumerate(rows) if len(row) != width]
        if bad: fail(f'ragged CSV {path.relative_to(ROOT)} rows={bad[:10]}')
    except Exception as exc: fail(f'invalid CSV {path.relative_to(ROOT)}: {exc}')
ok('CSV structure complete')

for path in active_files('*.py'):
    try: py_compile.compile(str(path), doraise=True)
    except Exception as exc: fail(f'Python compile failed {path.relative_to(ROOT)}: {exc}')
ok('Python compile complete')

# WBS/debt/trace dimensions.
def read_dicts(rel: str):
    with (ROOT / rel).open(encoding='utf-8-sig', newline='') as f: return list(csv.DictReader(f))

wbs = read_dicts('docs/v12_5/10_DETAILED_WBS_MASTER.csv') if (ROOT/'docs/v12_5/10_DETAILED_WBS_MASTER.csv').exists() else []
if len(wbs) != 336: fail(f'v12.5 WBS must contain 336 rows, found {len(wbs)}')
ids = [r.get('WBS_ID','') for r in wbs]
if len(ids) != len(set(ids)): fail('duplicate WBS_ID')
if sum(1 for r in wbs if r.get('WBS_ID','').startswith('AEG-V12-5-')) != 48: fail('v12.5 WBS delta must contain 48 rows')
if any(not r.get('Acceptance_Evidence','').strip() or not r.get('Primary_Owner','').strip() for r in wbs): fail('WBS blank acceptance/owner')
ok('WBS 336 rows, unique IDs, 48-row delta')

debt = read_dicts('docs/v12_5/11_TECH_DEBT_REGISTER.csv') if (ROOT/'docs/v12_5/11_TECH_DEBT_REGISTER.csv').exists() else []
if len(debt) != 68: fail(f'tech debt must contain 68 rows, found {len(debt)}')
if len({r.get('ID') for r in debt}) != len(debt): fail('duplicate tech debt ID')
ok('technical debt register 68 rows')

trace = read_dicts('docs/v12_5/12_TRACEABILITY_MATRIX.csv') if (ROOT/'docs/v12_5/12_TRACEABILITY_MATRIX.csv').exists() else []
for i in range(1, 13):
    if not any(r.get('Requirement_ID') == f'REQ-EXA-{i:03d}' for r in trace): fail(f'missing trace requirement REQ-EXA-{i:03d}')
ok('execution-assurance trace requirements complete')

# Fail-closed configuration and code markers.
settings = json.loads((ROOT/'src/KArtSell.Host/appsettings.json').read_text(encoding='utf-8'))
if settings.get('ModelOperations', {}).get('DispatcherEnabled') is not False: fail('ModelOperations dispatcher must default OFF')
program = (ROOT/'src/KArtSell.Host/Program.cs').read_text(encoding='utf-8')
for marker in ['modelOperationsDispatcherEnabled', '!modelOperationsDispatcherEnabled || x.ShadowEvaluation',
               'RegisterModelOperationsSchedules(modelOperationsDispatcherEnabled', '!x.AutomaticOrder', '!x.KisOrderAdapter']:
    if marker not in program: fail(f'missing fail-closed marker: {marker}')
scheduler = (ROOT/'src/KArtSell.Modules.ModelOperations/Scheduling/ModelOperationsScheduler.cs').read_text(encoding='utf-8')
for marker in ['if (!dispatcherEnabled)', 'RemoveIfExists', 'DispatcherJobId']:
    if marker not in scheduler: fail(f'missing scheduler safety marker: {marker}')
contract = (ROOT/'contracts/schedules/execution-assurance.v1.json')
if contract.exists():
    c = json.loads(contract.read_text(encoding='utf-8'))
    if c.get('dispatcher', {}).get('enabledByDefault') is not False: fail('schedule contract must default dispatcher OFF')
    if len(c.get('operations', [])) != 6: fail('execution-assurance contract must contain J25-J30')
ok('fail-closed model-operations boundary complete')

registry = (ROOT/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs').read_text(encoding='utf-8')
codes = re.findall(r'new\("(J\d+)"', registry)
if len(codes) != 16 or len(set(codes)) != 16: fail(f'model operation registry expected 16 unique operations, found {len(codes)}/{len(set(codes))}')
for code in [f'J{i}' for i in range(25,31)]:
    if code not in codes: fail(f'missing registry operation {code}')
ok('model operation registry J10-J30 approved set complete')

migration = (ROOT/'db/migrations/0017_execution_assurance.sql').read_text(encoding='utf-8')
for marker in ['V12_5_REAPPROVAL_REQUIRED', 'model_operation_artifact', 'release_evidence_bundle',
               'source_contract_snapshot', 'execution_assurance_decision', "'J30'"]:
    if marker not in migration: fail(f'migration 0017 missing marker: {marker}')
ok('migration 0017 execution-assurance structures complete')

# Prohibited patterns in active source.
source_files = [p for p in [*active_files('*.cs'), *active_files('*.sql')] if 'tests' not in p.relative_to(ROOT).parts]
patterns = {'AllowAnonymous()':'anonymous endpoint','IGenericRepository':'generic repository','DateTime.Now':'direct wall clock','DateTime.UtcNow':'direct wall clock'}
for token, label in patterns.items():
    hits = [str(p.relative_to(ROOT)) for p in source_files if token in p.read_text(encoding='utf-8')]
    if hits: fail(f'{label}: {token} in {hits}')
select_star = [str(p.relative_to(ROOT)) for p in source_files if re.search(r'\bselect\s+\*', p.read_text(encoding='utf-8'), flags=re.I)]
if select_star: fail(f'SELECT * prohibited: {select_star}')
ok('prohibited-pattern scan complete')

# Domain framework dependency scan and C# structural smoke.
for path in active_files('*.cs'):
    rel = path.relative_to(ROOT)
    text = path.read_text(encoding='utf-8')
    if 'Domain' in rel.parts:
        for token in ['using Dapper', 'using Npgsql', 'using FastEndpoints', 'using Hangfire', 'HttpContext', 'DbConnection']:
            if token in text: fail(f'domain framework dependency {token}: {rel}')
    scrub = re.sub(r'""".*?"""', '', text, flags=re.S)
    scrub = re.sub(r'"(?:\\.|[^"\\])*"', '', scrub)
    if scrub.count('{') != scrub.count('}'): fail(f'C# brace imbalance: {rel}')
ok('domain dependency and C# structural smoke complete')

# Package status honesty.
claims = []
for path in [ROOT/'README.md', ROOT/'RELEASE_NOTES_V12_5.md', ROOT/'docs/v12_5/00_EXECUTIVE_CRITICAL_REVIEW.md']:
    text = path.read_text(encoding='utf-8')
    for forbidden in ['PRODUCTION_READY', 'BUILD_VALIDATED / DB_REHEARSED', 'SHADOW_VALIDATED']:
        if forbidden in text: claims.append(f'{path.name}:{forbidden}')
if claims: fail(f'unsupported readiness claim: {claims}')
else: ok('readiness claims remain evidence-bounded')

# Toolchain warnings are expected in this container/package.
if not (ROOT/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml is still absent; frozen frontend validation remains blocked')
warn('.NET 10 restore/build/test is not executed by static validator')
warn('PostgreSQL DbUp fresh/upgrade/re-run/failure rehearsal is not executed by static validator')
warn('252-trading-day Shadow, PBO/DSR, security and DR evidence remain unmet')

print(f'PASS={len(PASSES)} WARN={len(WARNINGS)} FAIL={len(ERRORS)}')
for m in PASSES: print(f'PASS: {m}')
for m in WARNINGS: print(f'WARN: {m}')
for m in ERRORS: print(f'FAIL: {m}')
sys.exit(1 if ERRORS else 0)
