from __future__ import annotations
import csv, hashlib, re, sys
from pathlib import Path
from docx import Document

ROOT = Path(sys.argv[1] if len(sys.argv) > 1 else '.').resolve()
EXPECTED_HUMAN = 'v0.39.4-dev Consolidated'
EXPECTED_MACHINE = 'v0.39.4-dev'
issues=[]
passes=[]

def req(path: str):
    p=ROOT/path
    if p.exists(): passes.append(f'EXISTS: {path}')
    else: issues.append(f'MISSING: {path}')

required=[
'backend/manage.py','backend/config/settings.py','backend/requirements.txt','backend/templates/base.html',
'00-FREEZE-MIGRATIONS-WINDOWS.bat','00-freeze-migrations-linux.sh',
'01-SETUP-LOCAL-UAT-WINDOWS.bat','01-setup-local-uat-linux.sh','02-START-LOCAL-UAT-WINDOWS.bat','03-CHECK-LOCAL-UAT-WINDOWS.bat','04-RUN-SMOKE-TESTS-WINDOWS.bat',
'LOCAL-UAT-RUNTIME/tools/freeze_migrations.ps1','LOCAL-UAT-RUNTIME/tools/setup_local_uat.ps1','LOCAL-UAT-RUNTIME/tools/run_smoke_tests.ps1',
'05-COLLECT-RUNTIME-RESULTS-WINDOWS.bat','LOCAL-UAT-RUNTIME/tools/collect_runtime_results.ps1','MAINTENANCE-DEVELOPER-RUNTIME-GUIDE.txt','PC-RUNTIME-TEST-CHECKLIST.csv','PC-RUNTIME-TEST-START-HERE-BN.txt',
'PREFINAL-REVIEW-CORRECTION-LIST-v0.39.4.md','PREFINAL-REVIEW-CORRECTIONS-v0.39.4.csv',
'PREFINAL-REVIEW-STATIC-VERIFICATION-v0.39.4.txt',
'PREFINAL-REVIEW3-CORRECTION-LIST-v0.39.4.md','PREFINAL-REVIEW3-CORRECTIONS-v0.39.4.csv',
'PREFINAL-REVIEW3-STATIC-VERIFICATION-v0.39.4.txt','HRPAY-CONSOLIDATED-QA-TRACKER-QA001-QA111.csv',
'P0-STATIC-EVIDENCE.md','P1-STATIC-EVIDENCE.md','P2-STATIC-EVIDENCE.md',
'P0-RUNTIME-VERIFICATION-CHECKLIST.csv','P1-RUNTIME-VERIFICATION-CHECKLIST.csv','P2-RUNTIME-VERIFICATION-CHECKLIST.csv',
'FIXED-ITEMS-RUNTIME-REGRESSION-CHECKLIST.csv','QA-TRACKER-RECONCILIATION.md','PRE-FREEZE-MODEL-SCHEMA-AUDIT.txt','tools/regenerate_package_integrity.py',
'docs/HRPAY_Installation_Maintenance_Guide_BN.docx','docs/HRPAY_Installation_Maintenance_Guide_BN.pdf',
'docs/HRPAY_User_Manual_BN.docx','docs/HRPAY_User_Manual_BN.pdf',
'docs/HRPAY_Developer_Orientation_BN.docx','docs/HRPAY_Developer_Orientation_BN.pdf',
]
for x in required: req(x)

settings=(ROOT/'backend/config/settings.py').read_text(encoding='utf-8')
if 'APP_VERSION = "0.39.4-dev"' in settings: passes.append('APP_VERSION matches consolidated development version')
else: issues.append('APP_VERSION mismatch')
base=(ROOT/'backend/templates/base.html').read_text(encoding='utf-8')
if EXPECTED_HUMAN in base: passes.append('Base UI version matches consolidated development version')
else: issues.append('Base UI version mismatch')

for rel in ['LOCAL-UAT-RUNTIME/tools/freeze_migrations.ps1','LOCAL-UAT-RUNTIME/tools/setup_local_uat.ps1','LOCAL-UAT-RUNTIME/tools/run_smoke_tests.ps1','03-run-smoke-tests-linux.sh']:
    t=(ROOT/rel).read_text(encoding='utf-8-sig')
    if EXPECTED_MACHINE in t: passes.append(f'Runtime script version OK: {rel}')
    else: issues.append(f'Runtime script version mismatch: {rel}')

for rel in ['docs/HRPAY_Installation_Maintenance_Guide_BN.docx','docs/HRPAY_User_Manual_BN.docx','docs/HRPAY_Developer_Orientation_BN.docx']:
    d=Document(ROOT/rel)
    full='\n'.join(p.text for p in d.paragraphs)
    if EXPECTED_HUMAN in full: passes.append(f'DOCX version matches current reviewed build: {rel}')
    else: issues.append(f'DOCX version mismatch: {rel}')

preview=(ROOT/'OPEN-ME.html').read_text(encoding='utf-8-sig')
if EXPECTED_HUMAN in preview and 'STATIC PREVIEW · ' + EXPECTED_HUMAN in preview:
    passes.append('Static preview version matches current reviewed build')
else:
    issues.append('Static preview version mismatch')

collector=(ROOT/'LOCAL-UAT-RUNTIME/tools/collect_runtime_results.ps1').read_text(encoding='utf-8-sig')
if '.env.local' in collector and 'generated-migrations' in collector and 'Compress-Archive' in collector:
    passes.append('Runtime evidence collector present and excludes credential file by allow-list design')
else:
    issues.append('Runtime evidence collector contract mismatch')

# Stale version references are permitted only in explicit history files.
history={'CHANGELOG.txt','UI-PATCH-v0.37.1.txt','TRIAL-RELEASE-NOTES.txt','PACKAGE-CONTENTS.txt'}
for p in ROOT.rglob('*'):
    if not p.is_file() or p.suffix.lower() in {'.pdf','.docx','.zip','.png','.jpg','.jpeg','.pyc'}: continue
    if p.name in history or p.name in {'package_consistency_check.py','p2_static_verify.py','SHA256SUMS.txt'} or '__pycache__' in p.parts: continue
    try: t=p.read_text(encoding='utf-8-sig')
    except Exception: continue
    if re.search(r'v0\.37\.(0|1)',t):
        issues.append(f'Stale v0.37 reference outside history: {p.relative_to(ROOT)}')

# Normal setup must require frozen migrations; no ad-hoc makemigrations generation.
setup=(ROOT/'LOCAL-UAT-RUNTIME/tools/setup_local_uat.ps1').read_text(encoding='utf-8-sig')
if 'Frozen migration files are missing' in setup and 'makemigrations --check --dry-run' in setup and 'manage.py makemigrations --noinput' not in setup:
    passes.append('Windows setup consumes frozen migrations and performs drift check')
else:
    issues.append('Windows setup migration freeze contract mismatch')

linux_setup=(ROOT/'01-setup-local-uat-linux.sh').read_text(encoding='utf-8-sig')
bare_linux_generation = bool(re.search(r'(?m)^\s*python\s+manage\.py\s+makemigrations\s*$', linux_setup))
if 'Frozen migration files are missing' in linux_setup and 'makemigrations --check --dry-run' in linux_setup and not bare_linux_generation:
    passes.append('Linux setup consumes frozen migrations and performs drift check')
else:
    issues.append('Linux setup migration freeze contract mismatch')

runbook=(ROOT/'backend/docs/LOCAL-RUNTIME-RUNBOOK.md').read_text(encoding='utf-8-sig')
if 'v0.39.4-dev Consolidated smoke-test gate' in runbook and 'python manage.py makemigrations\n' not in runbook and 'migration baseline generation' not in runbook:
    passes.append('Runtime runbook follows frozen-migration policy')
else:
    issues.append('Runtime runbook contains stale migration/version guidance')

freeze_ps=(ROOT/'LOCAL-UAT-RUNTIME/tools/freeze_migrations.ps1').read_text(encoding='utf-8-sig')
if 'Django dependencies are missing/incomplete' in freeze_ps and 'manage.py check' in freeze_ps and 'makemigrations --check --dry-run' in freeze_ps:
    passes.append('Windows migration-freeze tool performs dependency + Django system preflight')
else:
    issues.append('Windows migration-freeze preflight safeguards missing')

freeze_linux=(ROOT/'00-freeze-migrations-linux.sh').read_text(encoding='utf-8-sig')
if "import django" in freeze_linux and 'manage.py check' in freeze_linux and 'makemigrations --check --dry-run' in freeze_linux:
    passes.append('Linux migration-freeze tool performs dependency + Django system preflight')
else:
    issues.append('Linux migration-freeze preflight safeguards missing')

prefreeze=(ROOT/'PRE-FREEZE-MODEL-SCHEMA-AUDIT.txt').read_text(encoding='utf-8-sig')
if 'RESULT: PASS - no static model/schema blocker detected.' in prefreeze and 'QA-011 remains OPEN' in prefreeze:
    passes.append('Pre-freeze model/schema audit evidence present with runtime limitation')
else:
    issues.append('Pre-freeze model/schema audit evidence missing/invalid')

backend_readme=(ROOT/'backend/README.md').read_text(encoding='utf-8-sig')
if 'generates the migration baseline' not in backend_readme and 'migration drift check' in backend_readme:
    passes.append('Backend README follows frozen-migration policy')
else:
    issues.append('Backend README contains stale migration guidance')

binding=(ROOT/'BACKEND-UI-BINDING-MATRIX.csv').read_text(encoding='utf-8-sig')
if 'freeze/review snapshot + drift check + migrate + seed + verify_runtime' in binding:
    passes.append('Backend/UI binding matrix follows frozen-migration policy')
else:
    issues.append('Backend/UI binding matrix contains stale migration guidance')


# Tracker coverage: all QA IDs must exist exactly once and the legacy FIXED set must have a runtime checklist.
tracker_path=ROOT/'HRPAY-DEVELOPER-ACTION-TRACKER-QA001-QA095.csv'
with tracker_path.open(encoding='utf-8-sig', newline='') as fh:
    tracker_rows=list(csv.DictReader(fh))
tracker_ids=[r.get('QA ID','').strip() for r in tracker_rows]
expected_ids=[f'QA-{i:03d}' for i in range(1,96)]
if tracker_ids == expected_ids and len(set(tracker_ids)) == 95:
    passes.append('QA tracker contains sequential QA-001..QA-095 with no duplicates')
else:
    issues.append('QA tracker ID sequence/uniqueness mismatch')

fixed_ids={r['QA ID'] for r in tracker_rows if r.get('Priority') == 'FIXED'}
fixed_check=(ROOT/'FIXED-ITEMS-RUNTIME-REGRESSION-CHECKLIST.csv')
with fixed_check.open(encoding='utf-8-sig', newline='') as fh:
    fixed_rows=list(csv.DictReader(fh))
fixed_check_ids={r.get('QA ID','').strip() for r in fixed_rows}
if fixed_ids and fixed_check_ids == fixed_ids:
    passes.append(f'Legacy FIXED runtime checklist covers all {len(fixed_ids)} items')
else:
    issues.append('Legacy FIXED runtime checklist coverage mismatch')

# Latest consolidated tracker must be sequential through QA-111.
consolidated_path=ROOT/'HRPAY-CONSOLIDATED-QA-TRACKER-QA001-QA111.csv'
with consolidated_path.open(encoding='utf-8-sig', newline='') as fh:
    consolidated_rows=list(csv.DictReader(fh))
consolidated_ids=[r.get('QA ID','').strip() for r in consolidated_rows]
expected_consolidated=[f'QA-{i:03d}' for i in range(1,112)]
if consolidated_ids == expected_consolidated and len(set(consolidated_ids)) == 111:
    passes.append('Latest consolidated QA tracker contains sequential QA-001..QA-111 with no duplicates')
else:
    issues.append('Latest consolidated QA tracker QA-001..QA-111 sequence/uniqueness mismatch')

# Review3 correction register must be complete.
review3_csv=ROOT/'PREFINAL-REVIEW3-CORRECTIONS-v0.39.4.csv'
with review3_csv.open(encoding='utf-8-sig', newline='') as fh:
    r3_rows=list(csv.DictReader(fh))
r3_id_key='ID' if r3_rows and 'ID' in r3_rows[0] else ('Correction ID' if r3_rows and 'Correction ID' in r3_rows[0] else None)
r3_ids=[r.get(r3_id_key,'').strip() for r in r3_rows] if r3_id_key else []
expected_r3=[f'R3-{i:03d}' for i in range(1,10)]
if r3_ids == expected_r3 and len(set(r3_ids)) == 9:
    passes.append('Review3 correction register contains R3-001..R3-009 with no duplicates')
else:
    issues.append(f'Review3 correction register mismatch: got={r3_ids}')

# Package integrity: SHA256SUMS covers every shipped file except itself.
sha_path=ROOT/'SHA256SUMS.txt'
if sha_path.exists():
    sha_entries={}
    parse_ok=True
    for line in sha_path.read_text(encoding='utf-8-sig').splitlines():
        if not line.strip():
            continue
        m=re.match(r'^([0-9a-fA-F]{64})\s+\*?\.?/?(.+)$', line.strip())
        if not m:
            parse_ok=False
            break
        sha_entries[m.group(2).replace('\\','/')]=m.group(1).lower()

    def shipped_files():
        out=set()
        for q in ROOT.rglob('*'):
            if not q.is_file():
                continue
            rel=q.relative_to(ROOT)
            if any(part in {'.venv','__pycache__','backups'} for part in rel.parts):
                continue
            if q.suffix.lower()=='.pyc' or q.name in {'.env.local','LOCAL-UAT-SMOKE-RESULT.txt','MIGRATION-FREEZE-RESULT.txt'}:
                continue
            out.add(rel.as_posix())
        return out

    shipped=shipped_files()
    expected_sha=shipped-{'SHA256SUMS.txt'}
    if parse_ok and set(sha_entries)==expected_sha:
        mismatch=[]
        for rel, expected_hash in sha_entries.items():
            h=hashlib.sha256((ROOT/rel).read_bytes()).hexdigest()
            if h != expected_hash:
                mismatch.append(rel)
        if not mismatch:
            passes.append(f'SHA256SUMS complete and valid: {len(sha_entries)} files')
        else:
            issues.append('SHA256 mismatch: ' + ', '.join(mismatch[:5]))
    else:
        missing=sorted(expected_sha-set(sha_entries))
        extra=sorted(set(sha_entries)-expected_sha)
        issues.append(f'SHA256 coverage mismatch (missing={missing[:5]}, extra={extra[:5]})')

# PACKAGE-CONTENTS intentionally excludes itself and SHA256SUMS to avoid cyclic metadata.
manifest_path=ROOT/'PACKAGE-CONTENTS.txt'
if manifest_path.exists():
    manifest_entries={}
    for line in manifest_path.read_text(encoding='utf-8-sig').splitlines():
        m=re.match(r'^(.*?)\t(\d+) bytes$', line)
        if m:
            manifest_entries[m.group(1).replace('\\','/')]=int(m.group(2))
    expected_manifest=shipped-{'PACKAGE-CONTENTS.txt','SHA256SUMS.txt'} if 'shipped' in locals() else set()
    bad_sizes=[rel for rel,size in manifest_entries.items() if not (ROOT/rel).exists() or (ROOT/rel).stat().st_size != size]
    if set(manifest_entries)==expected_manifest and not bad_sizes:
        passes.append(f'PACKAGE-CONTENTS complete and size-verified: {len(manifest_entries)} files')
    else:
        missing=sorted(expected_manifest-set(manifest_entries))
        extra=sorted(set(manifest_entries)-expected_manifest)
        issues.append(f'PACKAGE-CONTENTS mismatch (missing={missing[:5]}, extra={extra[:5]}, bad_sizes={bad_sizes[:5]})')

# Frozen migration snapshot is a runtime gate and is intentionally not required yet.
migrations=[p for p in (ROOT/'backend/apps').rglob('migrations/*.py') if p.name!='__init__.py']
if migrations: passes.append(f'Frozen migrations present: {len(migrations)} files')
else: passes.append('GATE OPEN (expected pre-freeze): no frozen migrations generated yet')

print('PACKAGE CONSISTENCY CHECK')
for x in passes: print('PASS:',x)
for x in issues: print('FAIL:',x)
print(f'RESULT: {"PASS" if not issues else "FAIL"} | pass={len(passes)} fail={len(issues)}')
sys.exit(0 if not issues else 1)
