"""Current dialogue definition candidates and accepted-occurrence guards.

A condition names a constraint. This module deliberately does not turn that
constraint into the general AcceptQuest/AchieveQuestObjective action's owner.
The current client action contains no such operand; the region-trigger mapping
has a narrower precondition. Import and these pure joins perform no game I/O.
"""
from copy import deepcopy
from datetime import datetime


SOURCE_GAP = 'direct_dialogue_action_to_quest_definition_source_rule_required'


def _oldest(*rows):
    return min(datetime.fromisoformat(r['observed_at'].replace('Z', '+00:00'))
               for r in rows).isoformat()


def _pending_receipt(receipt):
    if receipt is None:
        return None
    return receipt.get('planner_receipt', receipt)


def current_report_occurrence(frame, node, receipt=None):
    """Validate a report's actual accepted occurrence, including a saved send.

    This is an occurrence guard only. It does not establish action ownership
    or treat a changed step as the previous objective's individual success.
    """
    from .objective_binding import current_objective_occurrence
    state_current = (frame.get('quest_state_read') or {}).get('current_quest') or {}
    frame_current = frame.get('current_quest') or {}
    current = state_current or frame_current
    keys = ('quest_id', 'instance_id', 'accept_time', 'step_id', 'step_index', 'process_started_at')
    if (current.get('process_started_at') != frame.get('process_started_at') or
            (state_current and frame_current and any(state_current.get(k) != frame_current.get(k) for k in keys))):
        return False
    # The current report condition is matched to this node separately by the
    # dialogue binder. Only its index is added; all occurrence fields are read.
    scoped = dict(current, objective_index=node.get('objective_index'))
    return current_objective_occurrence(scoped, frame, node, _pending_receipt(receipt))


def observe_definition_context(dialogue, frame, node, receipt=None):
    """Preserve useful typed facts even when direct action ownership is absent.

    `candidate` is never an action proposal, a current NPC offer, or a completed
    quest. A waiting instance remains a waiting instance, with no AcceptTime.
    """
    from .arbeit_operations import _fresh
    from .dialogue_binding import dialogue_rules, validate_current_dialogue
    result = {'candidate': None, 'action_owner_verified': False,
              'classification': 'A_direct_action_owner_rule_missing', 'missing': []}
    gaps = validate_current_dialogue(dialogue, frame)
    if gaps:
        return dict(result, missing=gaps)
    rules = dialogue_rules()
    if dialogue.get('source_version') != rules['game_assembly_sha256']:
        return dict(result, missing=['dialogue_current_client_revision_required'])
    name_source = dialogue.get('static_name_binding') or {}
    if (name_source.get('verified') is not True or
            name_source.get('raw_sha256') != rules['static_name_binding']['raw_sha256']):
        return dict(result, missing=['current_QDBLPreloadData_asset_range_revision_required'])
    accept = node.get('op') == 'dialogue.accept'
    if not accept and node.get('op') != 'dialogue.report':
        return dict(result, missing=['accept_or_report_node_required'])
    play = dialogue['replicated']['play_data']
    expected_action = 'AcceptQuestDialogueActionData' if accept else 'AchieveQuestObjectiveDialogueActionData'
    expected_condition = 'QuestAcceptableDialogueConditionData' if accept else 'QuestObjectiveWaitingForAchieveDialogueConditionData'
    expected_prompt = 'AcceptQuest' if accept else 'AchieveQuestObjective'
    if (not isinstance(play.get('action'), dict) or play['action'].get('type') != expected_action or
            dialogue['replicated'].get('prompt_type', {}).get('name') != expected_prompt):
        return dict(result, missing=['current_exact_quest_action_and_prompt_required'])
    conditions = [c for c in play.get('conditions', []) if c.get('quest_name') is not None]
    name = rules['static_name_binding']['names'].get(str(node.get('quest_id')))
    if (not name or len(conditions) != 1 or conditions[0].get('type') != expected_condition or
            conditions[0].get('quest_name') != name):
        return dict(result, missing=['unique_current_definition_constraint_required'])
    if frame.get('quest_state_read') and frame['quest_state_read'].get('ok') is not True:
        return dict(result, missing=['current_owned_quest_collection_required'])
    now = dict(frame, now=frame.get('captured_at', frame.get('now')))
    npc = dialogue['player']['current_npc_id']
    if accept:
        rows = [r for r in frame.get('offers', []) if r.get('quest_id') == node.get('quest_id')]
        if len(rows) != 1:
            return dict(result, missing=['unique_current_waiting_definition_required'])
        current = rows[0]
        if (current.get('source') != 'current-root-typed-quest-collections' or current.get('confirmed') is not True or
                not _fresh(current, now) or current.get('process_started_at') != frame.get('process_started_at') or
                any(current.get(k) is not True for k in ('eligible', 'conditions_met', 'eligibility_verified', 'available_candidate')) or
                current.get('accept_time') not in (None, 0)):
            return dict(result, missing=['current_verified_waiting_definition_required'])
        giver = current.get('giver_definition') or {}
        if (giver.get('kind') != 'npc' or giver.get('declared_type') != 'Shared.Symbols.NPCSheetId' or
                type(giver.get('npc_id')) is not int or type(npc) is not int or
                (giver['npc_id'] & 0xffffffff) != (npc & 0xffffffff)):
            return dict(result, missing=['current_offer_sender_and_dialogue_npc_mismatch'])
        scope = {'ownership_scope': 'quest_definition_candidate', 'accepted_occurrence': False,
                 'waiting_instance_id': current.get('instance_id')}
    else:
        if (conditions[0].get('step_id') != node.get('step_id') or
                conditions[0].get('objective_index') != node.get('objective_index') or
                not current_report_occurrence(frame, node, receipt)):
            return dict(result, missing=['current_report_accepted_occurrence_required'])
        current = (frame.get('quest_state_read') or {}).get('current_quest') or frame['current_quest']
        scope = {k: current[k] for k in ('instance_id', 'accept_time', 'step_id', 'step_index')}
        scope.update(ownership_scope='accepted_occurrence_constraint', accepted_occurrence=True,
                     objective_index=node['objective_index'])
    source_version = current.get('source_version') or {}
    if not isinstance(source_version, dict) or any(source_version.get(k) != rules[k]
            for k in ('game_assembly_sha256', 'metadata_sha256')):
        return dict(result, missing=['current_quest_and_dialogue_source_revision_mismatch'])
    candidate = {**deepcopy(scope), 'quest_id': node['quest_id'], 'node_id': node['id'],
        'quest_name': name, 'npc_id': npc, 'dialogue_id': play['dialogue_id'],
        'dialogue_index': dialogue['replicated']['dialogue_index'],
        'prompt_index': dialogue['replicated']['prompt_index'],
        'definition_constraint': deepcopy(conditions[0]), 'definition_verified': True,
        'action_owner_verified': False, 'offered': False, 'accepted_effect': False,
        'source': 'current owned dialogue constraint joined to current typed quest definition',
        'source_revision': dialogue['source_version'], 'observed_at': _oldest(dialogue, current),
        **{k: frame.get(k) for k in ('session_id', 'character_id', 'process_started_at')},
        'basis': 'definition constraint only; general direct action target is not established'}
    return dict(result, candidate=candidate, missing=[SOURCE_GAP])
