from pathlib import Path
import json

STATE_VOCAB = {'USE','PILOT','NOT_NOW','UNKNOWN','EVIDENCE_INCOMPLETE','OWNER_MISSING','REVIEW_REQUIRED','HOLD','PARTIAL','DISPUTED'}
POSITIVE = {'USE','PILOT'}
AXES = {'CACHE','EPHEMERAL_COORDINATION','DURABLE_WORKFLOW'}
UNRESOLVED = {None,'','UNKNOWN','UNMEASURED','UNRESOLVED','NOT_ASSESSED','INCOMPLETE','EVIDENCE_INCOMPLETE','CONTRADICTORY'}


def _err(errors, rule, message):
    errors.append({'rule': rule, 'message': message})


def _bindings(root):
    raw = json.loads((root / 'fixtures/FIXTURE_BINDINGS.json').read_text(encoding='utf-8'))
    return {item['fixture_id']: json.loads((root / item['path']).read_text(encoding='utf-8')) for item in raw['fixtures']}


def _decisive_keys(axis):
    common = ['problem_class','state_semantics','simpler_alternative','simpler_alternative_result','operational_owner']
    if axis == 'CACHE':
        return common + ['latency_requirement','measured_current_latency','ttl_requirement','eviction_tolerance','persistence_requirement','failure_recovery_requirement']
    if axis == 'EPHEMERAL_COORDINATION':
        return common + ['latency_requirement','measured_current_latency','ttl_requirement','eviction_tolerance','coordination_need','failure_recovery_requirement']
    return common + ['workflow_duration','workflow_retry_need','failure_recovery_requirement','replay_or_history_need','persistence_requirement']


def _has_unresolved(axis, evidence):
    return any(evidence.get(key) in UNRESOLVED for key in _decisive_keys(axis))


def _trigger(axis, evidence):
    unresolved = _has_unresolved(axis, evidence)
    if axis == 'CACHE':
        present = (
            evidence.get('problem_class') == 'CACHE'
            and 'MISSES_TARGET' in str(evidence.get('measured_current_latency'))
            and evidence.get('simpler_alternative_result') == 'MISSES_DECLARED_TARGET'
        )
        absent = (
            not unresolved
            and 'MEETS_TARGET' in str(evidence.get('measured_current_latency'))
            and evidence.get('simpler_alternative_result') == 'MEETS_DECLARED_TARGET'
        )
    elif axis == 'EPHEMERAL_COORDINATION':
        present = (
            evidence.get('problem_class') == 'EPHEMERAL_COORDINATION'
            and evidence.get('coordination_need') == 'CROSS_INSTANCE_ATOMIC_INCREMENT'
            and 'MISSES_TARGET' in str(evidence.get('measured_current_latency'))
            and evidence.get('simpler_alternative_result') == 'MISSES_DECLARED_TARGET'
        )
        absent = (
            not unresolved
            and evidence.get('coordination_need') == 'NONE'
            and evidence.get('simpler_alternative_result') == 'MEETS_DECLARED_TARGET'
        )
    else:
        present = (
            evidence.get('problem_class') == 'DURABLE_WORKFLOW'
            and evidence.get('workflow_duration') == 'MULTI_DAY'
            and evidence.get('workflow_retry_need') == 'RETRYABLE_EXTERNAL_CALLS_REQUIRED'
            and evidence.get('failure_recovery_requirement') == 'RESUME_AFTER_PROCESS_OR_INFRA_FAILURE_REQUIRED'
            and evidence.get('replay_or_history_need') == 'INSPECTABLE_REPLAYABLE_HISTORY_REQUIRED'
            and evidence.get('simpler_alternative_result') == 'DATABASE_JOB_RUNNER_MISSES_RECOVERY_AND_HISTORY_TARGET'
        )
        absent = (
            not unresolved
            and evidence.get('workflow_duration') in {'UP_TO_5_MINUTES','UP_TO_6_HOURS'}
            and evidence.get('failure_recovery_requirement') in {'NO_DURABLE_RESUME_REQUIREMENT','CURRENT_JOB_RESUME_MEETS_SLO'}
            and evidence.get('replay_or_history_need') in {'NONE','AUDIT_LOG_ONLY'}
            and evidence.get('simpler_alternative_result') in {'MEETS_DECLARED_RECOVERY_TARGET','MEETS_DECLARED_TARGET'}
        )
    return unresolved, present, absent


def _positive_ready(axis, evidence):
    if axis == 'CACHE':
        return (
            evidence.get('state_semantics') == 'RECOMPUTABLE_COPY'
            and evidence.get('eviction_tolerance') == 'TOLERATED_RELOAD_FROM_SOURCE'
            and evidence.get('failure_recovery_requirement') == 'RELOAD_FROM_POSTGRESQL'
            and evidence.get('persistence_requirement') == 'NOT_REQUIRED_FOR_CACHE_COPY'
        )
    if axis == 'EPHEMERAL_COORDINATION':
        return (
            evidence.get('state_semantics') == 'ATOMIC_SHORT_LIVED_COUNTER'
            and evidence.get('ttl_requirement') == 'EXACT_60_SECONDS'
            and evidence.get('eviction_tolerance') == 'NO_EARLY_EVICTION_BEFORE_TTL'
            and evidence.get('failure_recovery_requirement') == 'FAIL_CLOSED_OR_BOUNDED_FALLBACK_DEFINED'
            and evidence.get('replay_or_history_need') == 'NONE'
        )
    return (
        evidence.get('state_semantics') == 'DURABLE_WORKFLOW_EXECUTION'
        and evidence.get('persistence_requirement') == 'DURABLE_EXECUTION_STATE_REQUIRED'
        and evidence.get('human_checkpoint_need') == 'MANUAL_APPROVAL_CHECKPOINTS_REQUIRED'
    )


def _authority_ok(axis, refs):
    if not isinstance(refs, list):
        return False
    got = set(refs)
    if axis in {'CACHE','EPHEMERAL_COORDINATION'}:
        required = {
            'source://AWR-002/REDISUSE','source://AWR-002/VALKEYINTRO',
            'lesson://AWR-002#claim/AWR-002-K1','lesson://AWR-002#claim/AWR-002-K2','lesson://AWR-002#claim/AWR-002-K4',
        }
    else:
        required = {'source://AWR-002/TEMPORAL','lesson://AWR-002#claim/AWR-002-K3','lesson://AWR-002#claim/AWR-002-K4'}
    return required <= got


def _token_basis(axis, evidence, tokens):
    for key in _decisive_keys(axis):
        value = evidence.get(key)
        if isinstance(value, str) and any(token in value.upper() for token in tokens):
            return True
    return False


def _typed_state_basis(axis, state, evidence):
    unresolved_count = sum(evidence.get(key) in UNRESOLVED for key in _decisive_keys(axis))
    resolved_count = len(_decisive_keys(axis)) - unresolved_count
    if state in {'UNKNOWN','EVIDENCE_INCOMPLETE'}:
        return unresolved_count > 0
    if state == 'OWNER_MISSING':
        return not evidence.get('operational_owner')
    if state == 'REVIEW_REQUIRED':
        return _token_basis(axis, evidence, {'CONFLICT','CONTRADICT','AUTHORITATIVE_NON_RECONSTRUCTABLE','EXCEEDS_TOLERANCE','STALE'})
    if state == 'HOLD':
        return _token_basis(axis, evidence, {'HOLD','BLOCKED','WAITING','PAUSED'})
    if state == 'PARTIAL':
        return unresolved_count > 0 and resolved_count > 0
    if state == 'DISPUTED':
        return _token_basis(axis, evidence, {'CONFLICT','CONTRADICT','DISPUTED'})
    return True


def validate_submission(submission, root=None):
    root = Path(root or Path(__file__).resolve().parent)
    errors = []
    fixtures = _bindings(root)
    fixture_id = submission.get('fixture_id')
    fixture = fixtures.get(fixture_id)
    if fixture is None:
        _err(errors, 'R3', 'Unknown fixture_id; select a shipped fixture.')
        return {'ok': False, 'errors': errors, 'fixture_id': fixture_id}

    records = submission.get('records')
    if not isinstance(records, list) or len(records) != 3:
        _err(errors, 'R1', 'Exactly three decision records are required.')
        return {'ok': False, 'errors': errors, 'fixture_id': fixture_id}
    axes = [record.get('decision_axis') for record in records]
    if set(axes) != AXES or len(set(axes)) != 3:
        _err(errors, 'R1', 'Decision axes must be exactly CACHE, EPHEMERAL_COORDINATION, and DURABLE_WORKFLOW.')
    by_axis = {record.get('decision_axis'): record for record in records}
    fixture_keys = {'CACHE':'cache','EPHEMERAL_COORDINATION':'ephemeral_coordination','DURABLE_WORKFLOW':'durable_workflow'}

    for axis in AXES:
        if axis not in by_axis:
            continue
        record = by_axis[axis]
        state = record.get('decision_state')
        evidence = record.get('evidence') if isinstance(record.get('evidence'), dict) else {}
        if state not in STATE_VOCAB:
            _err(errors, 'R2', f'{axis}: invalid decision_state.')
        if evidence != fixture['needs'][fixture_keys[axis]]:
            _err(errors, 'R3', f'{axis}: evidence does not exactly match the selected fixture.')
        for key in ['rationale','review_due','stop_or_pivot_trigger']:
            if not isinstance(record.get(key), str) or not record.get(key, '').strip():
                _err(errors, 'R11', f'{axis}: {key} is required.')
        if not _authority_ok(axis, evidence.get('authority_refs')):
            _err(errors, 'R10', f'{axis}: authority_refs do not contain the required FACT/DESIGN trace.')

        unresolved, trigger_present, trigger_absent = _trigger(axis, evidence)
        if state in POSITIVE:
            rule = {'CACHE':'R4','EPHEMERAL_COORDINATION':'R5','DURABLE_WORKFLOW':'R6'}[axis]
            if unresolved or not trigger_present or not _positive_ready(axis, evidence):
                _err(errors, rule, f'{axis}: positive state lacks a complete measured trigger or semantic fit.')
            if not evidence.get('operational_owner'):
                _err(errors, 'R10', f'{axis}: positive state requires an operational owner.')
            for key in ['migration_cost','failure_surface','evidence_ref']:
                if evidence.get(key) in UNRESOLVED:
                    _err(errors, 'R10', f'{axis}: positive state requires {key}.')
            if state == 'USE' and evidence.get('adoption_evidence') != 'VALIDATED_PRODUCTION_APPROVAL':
                _err(errors, 'R7', f'{axis}: USE requires validated adoption evidence; this fixture supports at most PILOT.')

        if state == 'NOT_NOW' and not trigger_absent:
            _err(errors, 'R8', f'{axis}: NOT_NOW requires measured trigger absence and a sufficient simpler alternative.')
        if unresolved and state in POSITIVE | {'NOT_NOW'}:
            _err(errors, 'R9', f'{axis}: unresolved decisive evidence requires a typed non-success state.')
        if state in {'UNKNOWN','EVIDENCE_INCOMPLETE','OWNER_MISSING','REVIEW_REQUIRED','HOLD','PARTIAL','DISPUTED'} and not _typed_state_basis(axis, state, evidence):
            _err(errors, 'R12', f'{axis}: {state} lacks its visible typed-state basis.')

    return {
        'ok': not errors,
        'errors': errors,
        'fixture_id': fixture_id,
        'rules_checked': [f'R{i}' for i in range(1, 13)],
        'checker_has_hidden_answer_key': False,
    }
