"""Durable observed item commitments, separate from current inventory facts."""
from copy import deepcopy

OWNER=('quest_id','instance_id','accept_time','process_started_at','character_id','session_id')
HUD_SOURCES=('get_quests','official:get_quests','official-cli:get_quests')
TYPED_SOURCE='current-root-typed-quest-collections'


def _same_occurrence(left,right):
    return all(left.get(k) is not None and left.get(k)==right.get(k) for k in OWNER)


def reservation_quantity(row):
    values=[row.get('required_quantity'),row.get('retained_minimum_quantity',0)]
    if any(type(v) is not int or v<0 for v in values):raise ValueError('goal_commitment_quantity_invalid')
    return max(values)


def reservation_current(row,snapshot,fresh):
    """Old requirement time is immutable; only occurrence validation is fresh."""
    current=row.get('current_occurrence_proof') or {}
    if snapshot.get('quest_state_read') and snapshot['quest_state_read'].get('ok') is not True:return False
    if (row.get('active') is not True or row.get('validated_by_current_occurrence') is not True
            or current.get('source')!=TYPED_SOURCE or current.get('state')!='accepted'
            or current.get('confirmed') is not True or not fresh(current,snapshot)
            or not _same_occurrence(row,current) or row.get('current_owned_at')!=current.get('observed_at')):
        return False
    if row.get('reservation_kind')=='typed_inventory_hold':
        from .objective_binding import native_hold_request_verified
        item=row.get('item') or {}
        return (row.get('item_domain')==item.get('table_domain') and row.get('item_id')==item.get('item_id')
                and native_hold_request_verified(row))
    return row.get('source') in HUD_SOURCES and bool(row.get('identity_binding'))


def goal_commitment_for_item(snapshot,item,*,exclude_node_id=None):
    """Other native goal commitments; neither inventory nor recipe deductions."""
    from .arbeit_operations import _fresh
    domain=item.get('table_domain');identity=item.get('item_id');name=item.get('name')
    rows=[];quantities={}
    for row in snapshot.get('goal_reservations',[]):
        if row.get('active') is not True or (exclude_node_id and row.get('node_id')==exclude_node_id):continue
        if row.get('reservation_kind')=='typed_inventory_hold':
            typed_item=row.get('item') or {}
            if typed_item.get('table_domain')!=domain or typed_item.get('item_id')!=identity:continue
            if row.get('item_name')!=name:raise ValueError('goal_commitment_item_identity_changed')
        elif row.get('item_name')!=name:continue
        if not reservation_current(row,snapshot,_fresh):raise ValueError('current_goal_commitment_occurrence_unverified')
        key=tuple(row[k] for k in OWNER)+tuple(row[k] for k in ('step_id','objective_index'))
        quantities[key]=max(quantities.get(key,0),reservation_quantity(row));rows.append(deepcopy(row))
    if any(g.get('item_name')==name for g in snapshot.get('goal_reservation_gaps',[])):
        raise ValueError('current_goal_commitment_quantity_unresolved')
    return dict(required_quantity=sum(quantities.values()),evidence=rows,
                source='durable_current_occurrence_native_goal_commitments',inventory_quantity_modified=False)


def reconcile_reservations(graph,stored,snapshot,accept_time,fresh):
    from .objective_binding import native_hold_request_verified
    rows=deepcopy(list(stored));gaps=[]
    proofs=[p for p in snapshot.get('state_proofs',[]) if type(accept_time) is int and accept_time>0
        and p.get('quest_id')==graph['quest_id'] and p.get('confirmed') is True
        and p.get('source')==TYPED_SOURCE and p.get('accept_time')==accept_time and fresh(p,snapshot)]
    if snapshot.get('quest_state_read') and snapshot['quest_state_read'].get('ok') is not True:proofs=[]
    accepted=[p for p in proofs if p.get('state')=='accepted' and type(p.get('instance_id')) is int
        and p['instance_id']>0 and p.get('process_started_at')]
    current=accepted[0] if len({tuple(p.get(k) for k in OWNER) for p in accepted})==1 else None
    completed=next((p for p in proofs if p.get('state')=='completed' and p.get('process_started_at')),None)
    for row in rows:
        valid=bool(current and _same_occurrence(row,current))
        row.update(validated_by_current_occurrence=valid,current_owned_at=current['observed_at'] if valid else None,
                   current_occurrence_proof=deepcopy(current) if valid else None)
        if (completed and all(row.get(k)==completed.get(k) for k in OWNER if k!='instance_id')
                and (completed.get('instance_id') is None or completed['instance_id']==row.get('instance_id'))):
            row.update(active=False,release_evidence=deepcopy(completed))
    if completed:return rows,[]
    # Prefer typed data for the same objective; HUD is not a second quantity.
    node_ids={n['id']:n for n in graph['nodes'] if n.get('objective_index') is not None}
    typed={};invalid=set()
    for request in snapshot.get('inventory_hold_requests',[]):
        node=node_ids.get(request.get('node_id'))
        if not node:continue
        key=(node['step_id'],node['objective_index'])
        valid=(current and native_hold_request_verified(request) and _same_occurrence(request,current)
            and fresh(request,snapshot) and fresh(request['native_effect'],snapshot)
            and all(request.get(k)==node.get(k) for k in ('quest_id','step_id','objective_index')))
        if not valid:
            invalid.add(key);gaps.append(dict(item_name=request.get('item_name'),reason='typed_hold_reservation_unverified'));continue
        prior=typed.get(key)
        if prior and any(prior.get(k)!=request.get(k) for k in ('item','required_quantity','constraints')):
            invalid.add(key);gaps.append(dict(item_name=request['item_name'],reason='typed_hold_reservation_conflict'));continue
        typed[key]=request
    candidates=[]
    for key,request in typed.items():
        if key in invalid:continue
        item=request['item']
        candidates.append(dict(deepcopy(request),reservation_kind='typed_inventory_hold',
            item_domain=item['table_domain'],item_id=item['item_id'],item_category=item.get('category')))
    for proof in snapshot.get('objective_evidence',[]):
        key=(proof.get('step_id'),proof.get('objective_index'))
        if key in typed or key in invalid:continue
        if (not current or proof.get('quest_id')!=graph['quest_id'] or proof.get('semantics')!='inventory.hold'
                or proof.get('source') not in HUD_SOURCES or not proof.get('identity_binding') or not proof.get('item_name')
                or not _same_occurrence(proof,current) or not fresh(proof,snapshot)
                or type(proof.get('required_quantity')) is not int or proof['required_quantity']<0
                or any(k is None for k in key)):
            continue
        candidates.append(dict(deepcopy(proof),reservation_kind='owned_HUD_hold'))
    for proof in candidates:
        key=(proof['step_id'],proof['objective_index'])
        matches=[r for r in rows if _same_occurrence(r,proof) and (r['step_id'],r['objective_index'])==key]
        if len(matches)>1:
            gaps.append(dict(item_name=proof['item_name'],reason='durable_objective_reservation_ambiguous'));continue
        existing=matches[0] if matches else None
        if existing and (existing['item_name']!=proof['item_name'] or
                (existing.get('reservation_kind')=='typed_inventory_hold' and existing.get('item')!=proof.get('item'))):
            gaps.append(dict(item_name=proof['item_name'],reason='durable_objective_item_identity_changed'));continue
        if existing and existing.get('reservation_kind')=='typed_inventory_hold' and proof['reservation_kind']!='typed_inventory_hold':continue
        if existing is None:
            rows.append(dict(proof,active=True,validated_by_current_occurrence=True,
                current_owned_at=current['observed_at'],current_occurrence_proof=deepcopy(current)))
        elif (proof['required_quantity']>existing['required_quantity'] or
                (proof['reservation_kind']=='typed_inventory_hold' and existing.get('reservation_kind')!='typed_inventory_hold')):
            history=existing.get('previous_observations',[])+[{k:deepcopy(existing.get(k)) for k in
                ('source','observed_at','required_quantity','item','reservation_kind')}]
            # Preserve the old minimum without falsifying the new native goal.
            minimum=reservation_quantity(existing)
            existing.update(proof,previous_observations=history,retained_minimum_quantity=minimum,
                active=True,validated_by_current_occurrence=True,current_owned_at=current['observed_at'],
                current_occurrence_proof=deepcopy(current))
    names={t['table_data']['display_name'] for n in graph['nodes'] for t in n.get('params',{}).get('targets',[])
        if t.get('kind')=='item' and t.get('table_data',{}).get('display_name')}
    reserved={r['item_name'] for r in rows if r.get('active') is True}
    gaps.extend(dict(item_name=name,reason='current_goal_quantity_not_observed') for name in sorted(names-reserved))
    return rows,gaps
