#!/usr/bin/env python3
"""Disjoint controller ownership over every registered HTTP key; no coverage inflation."""
import argparse
import csv
import hashlib
import io
from pathlib import Path
import integrate_20261010 as base

HERE = Path(__file__).resolve().parent
FIELDS = ['owner', 'repo', 'method', 'path', 'active_batches', 'matrix_gate',
          'success_evidence', 'mysql_evidence', 'dm_evidence', 'source_evidence']
SESSION_OWNERS = {
    ('store', '/p/ontoanalysis'): 'session-store-nutrition',
    ('unreviewed', '/p/staff'): 'session-store-nutrition',
    ('store', '/p/zgyh'): 'session-store-nutrition',
    ('unreviewed', '/p/zgyh'): 'session-store-nutrition',
    ('unreviewed', '/p/datemenu'): 'session-store-nutrition',
    ('store', '/p/bill'): 'session-export',
    ('unreviewed', '/p/bill'): 'session-export',
    ('store', '/p/download'): 'session-store-nutrition',
    ('unreviewed', '/p/download'): 'session-store-nutrition',
    ('store', '/p/workflow'): 'session-store-nutrition',
    ('unreviewed', '/p/workflow'): 'session-store-nutrition',
    ('store', '/p/api'): 'session-ai-exercise',
    ('unreviewed', '/p/api'): 'session-ai-exercise',
    ('store', '/p/weightstatis'): 'session-export',
    ('unreviewed', '/p/weightstatis'): 'session-export',
    ('store', '/p/supplier'): 'session-store-nutrition',
    ('unreviewed', '/p/supplier'): 'session-store-nutrition',
    ('store', '/p/stageanalyze'): 'session-ai-exercise',
    ('unreviewed', '/p/stageanalyze'): 'session-ai-exercise',
    ('store', '/p/sportconsume'): 'session-ai-exercise',
    ('unreviewed', '/p/sportconsume'): 'session-ai-exercise',
    ('store', '/p/sportsevaluation'): 'session-ai-exercise',
    ('unreviewed', '/p/sportsevaluation'): 'session-ai-exercise',
    ('store', '/p/psychologicalevaluation'): 'session-ai-exercise',
    ('unreviewed', '/p/psychologicalevaluation'): 'session-ai-exercise',
    ('store', '/m/onemeal'): 'session-ai-exercise',
    ('unreviewed', '/m/onemeal'): 'session-ai-exercise',
    ('store', '/p/mealorder'): 'session-export',
    ('unreviewed', '/p/mealorder'): 'session-export',
    ('store', '/p/food'): 'session-store-nutrition',
    ('unreviewed', '/p/food'): 'session-store-nutrition',
    ('store', '/p/department'): 'session-ai-exercise',
    ('unreviewed', '/p/department'): 'session-ai-exercise',
    ('store', '/p/operationlog'): 'session-ai-exercise',
    ('unreviewed', '/p/operationlog'): 'session-ai-exercise',
    ('store', '/p/purchase'): 'session-export',
    ('unreviewed', '/p/purchase'): 'session-export',
    ('store', '/p/meal'): 'session-export',
    ('unreviewed', '/p/meal'): 'session-export',
    ('store', '/api/marketingstrategy'): 'session-ai-exercise',
    ('unreviewed', '/api/marketingstrategy'): 'session-ai-exercise',
    ('store', '/api/consumelimit'): 'session-store-nutrition',
    ('unreviewed', '/api/consumelimit'): 'session-store-nutrition',
    ('store', '/store/dishes.pushrule'): 'session-ai-exercise',
    ('store', '/m/article'): 'session-export',
    ('unreviewed', '/m/article'): 'session-export',
    ('ai_api', '/store/dishes.pushrule'): 'session-ai-exercise',
    ('unreviewed', '/store/dishes.pushrule'): 'session-ai-exercise',
    ('ai_api', '/store/enum'): 'session-export',
    ('unreviewed', '/store/enum'): 'session-export',
    ('ai_api', '/store/dishes.dishes'): 'session-ai-exercise',
    ('unreviewed', '/store/dishes.dishes'): 'session-ai-exercise',
    ('ai_api', '/terminal/dishes.dishes'): 'session-store-nutrition',
    ('ai_api', '/store/dishes.foods'): 'session-export',
    ('unreviewed', '/store/dishes.foods'): 'session-export',
    ('ai_api', '/api/captcha'): 'session-ai-exercise',
    ('unreviewed', '/p/test'): 'session-export',
    ('store', '/p/test'): 'session-export',
    ('ai_api', '/api/wellandscale'): 'session-ai-exercise',
    ('ai_api', '/admin/setting.timer'): 'session-ai-exercise',
    ('unreviewed', '/p/warehouse'): 'session-ai-exercise',
    ('store', '/p/warehouse'): 'session-ai-exercise',
    ('unreviewed', '/p/user_score'): 'next_store_read_batch',
    ('store', '/p/user_score'): 'next_store_read_batch',
    ('store', '/api/datacenterdashboard'): 'session-export',
    ('store', '/p/cater'): 'session-ai-exercise',
    ('unreviewed', '/p/cater'): 'session-ai-exercise',
    ('store', '/testhealth/ping'): 'next_store_read_batch',
    ('store', '/testhealth/echopayload'): 'next_store_read_batch',
    ('ai_api', '/admin/setting.science'): 'session-ai-exercise',
    ('ai_api', '/admin/store.equipment'): 'session-export',
    ('store', '/m/user'): 'next_store_read_batch',
    ('unreviewed', '/m/user'): 'next_store_read_batch',
    ('store', '/m/workflow'): 'session-store-nutrition',
    ('unreviewed', '/m/workflow'): 'session-store-nutrition',
    ('ai_api', '/api/expert.qrcode'): 'resume_ai_read_tests',
    ('store', '/p/systemconfig'): 'resume_store_evidence',
    ('unreviewed', '/p/systemconfig'): 'resume_store_evidence',
    ('ai_api', '/api/passport'): 'session-ai-exercise',
    ('store', '/api/identity'): 'resume_ai_read_tests',
    ('unreviewed', '/api/identity'): 'resume_ai_read_tests',
    ('ai_api', '/admin/store'): 'session-ai-exercise',
    ('store', '/api/subsidy'): 'session-store-nutrition',
    ('unreviewed', '/api/subsidy'): 'session-store-nutrition',
    ('store', '/p/activity'): 'resume_ai_read_tests',
    ('unreviewed', '/p/activity'): 'resume_ai_read_tests',
    ('store', '/m/nutrition'): 'session-export',
    ('unreviewed', '/m/nutrition'): 'session-export',
    ('store', '/api/subsidyrule'): 'session-ai-exercise',
    ('unreviewed', '/api/subsidyrule'): 'session-ai-exercise',
    ('store', '/p/ai'): 'resume_ai_read_tests',
    ('unreviewed', '/p/ai'): 'resume_ai_read_tests',
    ('store', '/p/material'): 'resume_ai_read_tests',
    ('unreviewed', '/p/material'): 'resume_ai_read_tests',
    ('store', '/api/orderevaluate'): 'resume_ai_read_tests',
    ('unreviewed', '/api/orderevaluate'): 'resume_ai_read_tests',
    ('store', '/api/card'): 'session-export',
    ('unreviewed', '/api/card'): 'session-export',
    ('store', '/p/birthdaydish'): 'session-store-nutrition',
    ('store', '/p/datemenu'): 'session-store-nutrition',
    ('store', '/m/datemenu'): 'session-store-nutrition',
    ('store', '/m/date_menu'): 'session-store-nutrition',
    ('store', '/p/metabolismpal'): 'session-store-nutrition',
    ('store', '/m/weeknew'): 'session-store-nutrition',
    ('store', '/m/staff'): 'session-store-nutrition',
    ('store', '/p/staff'): 'session-store-nutrition',
    ('store', '/p/medicalfiles'): 'session-store-nutrition',
    ('ai_api', '/store/investigate.nutritionlack'): 'session-ai-exercise',
    ('ai_api', '/api/usermotion'): 'session-ai-exercise',
    ('ai_api', '/api/yingjiduo.record'): 'session-ai-exercise',
    ('ai_api', '/api/yingjiduo.user'): 'session-ai-exercise',
    ('ai_api', '/api/plate'): 'session-ai-exercise',
    ('ai_api', '/api/yingjiduo.auth'): 'session-ai-exercise',
    ('ai_api', '/api/dishes'): 'session-ai-exercise',
    ('ai_api', '/api/health.suggestion'): 'session-ai-exercise',
    ('ai_api', '/api/bank'): 'session-ai-exercise',
    ('ai_api', '/terminal/store.face'): 'session-ai-exercise',
    ('ai_api', '/terminal/equipment'): 'session-ai-exercise',
    ('ai_api', '/terminal/setting'): 'session-ai-exercise',
    ('ai_api', '/store/store'): 'session-ai-exercise',
    ('ai_api', '/terminal/store'): 'session-ai-exercise',
    ('ai_api', '/store/order.export'): 'session-export',
    ('ai_api', '/terminal/order.export'): 'session-export',
    ('store', '/p/cashrefund'): 'session-export',
    ('store', '/p/supplementmealorder'): 'session-export',
    ('store', '/p/cashoutorder'): 'session-export',
    ('store', '/p/consumesubsidy'): 'session-export',
    ('store', '/p/cashrefundlower'): 'session-export',
    ('store', '/p/cashoutorderexportlower'): 'session-export',
    ('store', '/p/supplementmealorderlower'): 'session-export',
    ('unreviewed', '/p/cashoutorder'): 'session-export',
}


def methods(value):
    return sorted(set(x.strip().upper() for x in value.split('|') if x.strip()))


def group(repo, path):
    return repo, '/'.join(base.norm(path).split('/')[:3])


def records():
    _, contracts, _ = base.read_csv(base.CONTRACT)
    _, coverage, _ = base.read_csv(base.COVERAGE)
    groups = {}
    for row in contracts:
        for method in methods(row['method']):
            groups.setdefault((row['repo'], method, base.norm(row['path'])), []).append(row)
    coverage_keys = {(r['repo'], m, base.norm(r['route_or_command']))
                     for r in coverage if r['kind'] == 'http' for m in methods(r['method'])}
    if set(groups) != coverage_keys:
        raise ValueError('Coverage/contract keys differ; reconcile before assigning')
    with (HERE / 'active-claims.csv').open(newline='') as stream:
        claims = list(csv.DictReader(stream))
    active = [r for r in claims if r['state'] in {'running', 'queued', 'awaiting-review'}]
    controller_owners = {}
    claim_keys = set()
    for claim in active:
        key = claim['repo'], claim['method'].upper(), base.norm(claim['path'])
        if key in claim_keys:
            raise ValueError('Duplicate active claim: ' + repr(key))
        if any(old[0] == key[0] and old[2] == key[2]
               and ('*' in {old[1], key[1]}) for old in claim_keys):
            raise ValueError('Wildcard claim overlaps concrete method: ' + repr(key))
        claim_keys.add(key)
        if key not in groups and (key[0], '*', key[2]) not in groups:
            raise ValueError('Claim absent from inventory: ' + repr(key))
        module = group(claim['repo'], claim['path'])
        if module in SESSION_OWNERS and claim['owner'] != SESSION_OWNERS[module]:
            raise ValueError('Independent session owns controller: ' + repr(module))
        if module in controller_owners and controller_owners[module] != claim['owner']:
            raise ValueError('Controller shared by active writers: ' + repr(module))
        controller_owners[module] = claim['owner']
    rows = []
    for (repo, method, path), sources in sorted(groups.items()):
        module = group(repo, path)
        if repo == 'ai_api':
            owner = 'resume_ai_read_tests'
        elif repo == 'store':
            slot = int(hashlib.sha256(module[1].encode()).hexdigest(), 16) % 2
            owner = ['resume_store_evidence', 'review_equipment237'][slot]
        else:
            owner = 'root-inventory-triage'
        owner = SESSION_OWNERS.get(module, controller_owners.get(module, owner))
        aggregate = lambda field: ';'.join(sorted({r.get(field, '') for r in sources if r.get(field)}))
        batches = sorted({c['batch'] for c in active if c['repo'] == repo
                          and base.norm(c['path']) == path
                          and (method == '*' or c['method'].upper() == method)})
        rows.append(dict(owner=owner, repo=repo, method=method, path=path,
                         active_batches=';'.join(batches), matrix_gate=aggregate('coverage_gate'),
                         success_evidence=aggregate('success_case'),
                         mysql_evidence=aggregate('mysql_input_and_readback'),
                         dm_evidence=aggregate('dm_input_and_readback'),
                         source_evidence=aggregate('evidence')))
    assert len(rows) == len(groups) == len(coverage_keys)
    return rows, len(active)


def render(rows):
    out = io.StringIO(newline='')
    writer = csv.DictWriter(out, FIELDS, lineterminator='\n')
    writer.writeheader()
    writer.writerows(rows)
    return out.getvalue()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--check', action='store_true')
    args = parser.parse_args()
    rows, active = records()
    target = HERE / 'parallel-work-queue.csv'
    expected = render(rows)
    if args.check:
        if target.read_text() != expected:
            raise SystemExit('Queue stale; Root must regenerate')
    else:
        target.write_text(expected)
    print('registered_keys=%d active_claims=%d duplicate_claims=0 missing_keys=0' % (len(rows), active))
