#!/usr/bin/env python3
from __future__ import annotations
from pathlib import Path
import csv, hashlib, json, re, sys

root = Path(__file__).resolve().parents[1]
errors: list[str] = []
warnings: list[str] = []

def fail(message: str) -> None: errors.append(message)
def warn(message: str) -> None: warnings.append(message)
def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open('rb') as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b''):
            digest.update(block)
    return digest.hexdigest()

def read_csv(relative: str) -> list[dict[str, str]]:
    path = root / relative
    if not path.exists():
        fail(f'missing CSV {relative}')
        return []
    with path.open(encoding='utf-8-sig', newline='') as stream:
        return list(csv.DictReader(stream))

# JSON syntax and unresolved generation tokens.
for path in root.rglob('*.json'):
    try:
        json.loads(path.read_text(encoding='utf-8-sig'))
    except Exception as exc:
        fail(f'JSON {path.relative_to(root)}: {exc}')
for path in [root/'frontend/src/shared/ui', root/'docs/v13_0']:
    if not path.exists(): continue
    for item in path.rglob('*'):
        if item.is_file() and item.suffix.lower() in {'.ts','.vue','.cs','.md','.sql'}:
            text=item.read_text(encoding='utf-8')
            if re.search(r'__[A-Z][A-Z0-9_]+__', text):
                fail(f'unresolved template token: {item.relative_to(root)}')

# WBS/debt/decision integrity.
wbs = read_csv('docs/v13_0/08_DETAILED_WBS_MASTER.csv')
if len(wbs) != 400: fail(f'WBS count {len(wbs)} != 400')
wbs_ids = [row.get('WBS_ID','') for row in wbs]
if any(not value for value in wbs_ids): fail('blank WBS ID')
if len(wbs_ids) != len(set(wbs_ids)): fail('duplicate WBS IDs')
for required in ('Task','Artifact','Acceptance_Evidence','Primary_Owner','PD','Gate','Risk','Status'):
    if any(not row.get(required,'').strip() for row in wbs): fail(f'blank WBS field: {required}')

debts = read_csv('docs/v13_0/TECH_DEBT_REGISTER.csv')
if len(debts) != 80: fail(f'tech debt count {len(debts)} != 80')
decisions = read_csv('docs/v13_0/DECISION_LOG.csv')
if len(decisions) != 52: fail(f'decision count {len(decisions)} != 52')

# FE catalogue, adapter boundary and standard assets.
catalogue = root/'frontend/src/shared/ui/screen-types/catalogue.ts'
if not catalogue.exists(): fail('missing FE screen catalogue')
else:
    text=catalogue.read_text(encoding='utf-8')
    for number in range(1,11):
        if f"id: 'T{number:02d}'" not in text: fail(f'missing T{number:02d}')
for name in ('AppShellLayout','PageLayout','ReviewWorkbenchLayout','FormPageLayout','DashboardLayout'):
    if not (root/f'frontend/src/shared/ui/layouts/{name}.vue').exists(): fail(f'missing layout {name}')
for name in ('KsButton','KsTextField','KsTextArea','KsSelect','KsCheckbox','KsDialog','KsStatusTag','KsDataGrid'):
    if not (root/f'frontend/src/shared/ui/components/{name}.vue').exists(): fail(f'missing standard component {name}')
for path in (root/'frontend/src').rglob('*'):
    if path.suffix not in ('.ts','.vue'): continue
    text=path.read_text(encoding='utf-8')
    if re.search(r"from ['\"](primevue/|ag-grid)", text) and '/shared/ui/adapter/primevue/' not in path.as_posix():
        fail(f'vendor import outside adapter: {path.relative_to(root)}')

# Model-operation registry/contract/migration fail-closed checks.
registry_path=root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationDefinition.cs'
registry=registry_path.read_text(encoding='utf-8') if registry_path.exists() else ''
contract_path=root/'contracts/schedules/model-operations.v2.json'
contract=json.loads(contract_path.read_text(encoding='utf-8')) if contract_path.exists() else {}
migration_path=root/'db/migrations/0018_v13_model_feedback_loop.sql'
migration=migration_path.read_text(encoding='utf-8') if migration_path.exists() else ''
for number in range(31,39):
    code=f'J{number}'
    if f'"{code}"' not in registry: fail(f'missing registry {code}')
    if not any(item.get('operationCode')==code and item.get('enabledByDefault') is False for item in contract.get('operations',[])):
        fail(f'missing disabled contract {code}')
    if f"'{code}'" not in migration: fail(f'missing migration schedule {code}')
if 'enabled, next_due_at' not in migration or migration.count(',2,false,') < 8: fail('J31-J38 schedules are not explicitly disabled')
for marker in ('maturity_revision','supersedes_maturity_id','supersedes_hypothesis_id','prevent_append_only_change'):
    if marker not in migration: fail(f'missing append-only marker {marker}')
boundary=(root/'src/KArtSell.Modules.ModelOperations/Domain/ModelOperationExecutionBoundary.cs').read_text(encoding='utf-8')
if 'NO_AUTO_MODEL_OR_ORDER_MUTATION' not in boundary: fail('missing model operation fail-closed boundary')
for forbidden in contract.get('automationBoundary',{}).get('forbidden',[]):
    if forbidden not in {'AUTO_PROMOTE','AUTO_ROLLBACK','AUTO_PARAMETER_CHANGE','AUTO_ORDER','KIS_SUBMISSION','CLIENT_PUBLICATION'}:
        fail(f'unexpected automation boundary token {forbidden}')

# Current-request attachment byte-for-byte index.
index_path=root/'attachments/SOURCE_INDEX_V13_0.json'
if not index_path.exists(): fail('missing SOURCE_INDEX_V13_0.json')
else:
    index=json.loads(index_path.read_text(encoding='utf-8'))
    files=index.get('files',[])
    if len(files)!=5 or index.get('all_match') is not True: fail('current request attachment index is incomplete')
    for item in files:
        package_path=root/item['package_path']
        if not package_path.exists(): fail(f"missing attachment {item['package_path']}")
        elif package_path.stat().st_size != item['size'] or sha256(package_path) != item['sha256']:
            fail(f"attachment hash mismatch {item['package_path']}")

# Scaffolder and document evidence files.
for relative in (
    'tools/scaffold_vertical_slice.py',
    'templates/vertical-slice/backend/Endpoint.cs',
    'templates/vertical-slice/frontend/pages/__FEATURE__Page.vue',
    'SCAFFOLD_VALIDATION_V13_0.txt',
    'FRONTEND_SYNTAX_VALIDATION_V13_0.txt',
    'DOCX_QA_V13_0.txt',
    'docs/v13_0/K-ArtSell_Aegis_v13_0_표준UI_지속모델운영_통합고도화_제안서.docx'):
    if not (root/relative).exists(): fail(f'missing evidence/artifact {relative}')
syntax_path=root/'FRONTEND_SYNTAX_VALIDATION_V13_0.txt'
if syntax_path.exists() and 'SYNTAX_ERRORS=0' not in syntax_path.read_text(encoding='utf-8'): fail('frontend syntax validation not clean')
qa_path=root/'DOCX_QA_V13_0.txt'
if qa_path.exists() and 'Visual inspection: PASS' not in qa_path.read_text(encoding='utf-8'): fail('DOCX visual QA not passed')

# Runtime evidence remains deliberately external.
if not (root/'frontend/pnpm-lock.yaml').exists(): warn('pnpm-lock.yaml missing; frozen frontend validation not possible')
warn('.NET SDK/PostgreSQL runtime validation must be executed in approved CI')

print(f'PASS={0 if errors else 1} WARN={len(warnings)} FAIL={len(errors)}')
for message in warnings: print('WARN', message)
for message in errors: print('FAIL', message)
sys.exit(1 if errors else 0)
