#!/usr/bin/env python3
"""Inspect fictional prepared buttons and exercise pinned SDK serialization.

Requires slack-sdk==3.45.0. No WebClient, token, message or ERP operation.
Routes below are predictions from the consulted button contract, assuming a
click and a supplied user's Slackbot availability; no native click is executed.
Local role/handler qualification and JSON paths are not Slack payload fields.
"""
import copy
from collections import Counter
from contextlib import contextmanager
import io
import json
import logging
import socket
import sys


PINNED_SDK_VERSION = '3.45.0'


@contextmanager
def refuse_connections():
    counter = {'attempts': 0}
    original_connect = socket.socket.connect
    original_connect_ex = socket.socket.connect_ex
    original_bytecode = sys.dont_write_bytecode
    def deny(*args, **kwargs):
        counter['attempts'] += 1
        raise AssertionError('Network connection forbidden in this local exercise')
    socket.socket.connect = deny
    socket.socket.connect_ex = deny
    sys.dont_write_bytecode = True
    try:
        yield counter
    finally:
        socket.socket.connect = original_connect
        socket.socket.connect_ex = original_connect_ex
        sys.dont_write_bytecode = original_bytecode


def text_id(value):
    return type(value) is str and bool(value.strip()) and value == value.strip()


def button_locations(message):
    if type(message) is not dict or type(message.get('blocks')) is not list:
        raise ValueError('outside_local_prepared_message')
    found = []
    for index, block in enumerate(message['blocks']):
        if type(block) is not dict:
            raise ValueError('block_must_be_local_object')
        if block.get('type') == 'actions':
            if type(block.get('elements')) is not list:
                raise ValueError('actions_elements_not_qualified')
            for number, element in enumerate(block['elements']):
                if type(element) is not dict:
                    raise ValueError('element_must_be_local_object')
                if element.get('type') == 'button':
                    found.append((f'/blocks/{index}/elements/{number}', index, element))
        elif block.get('type') == 'section':
            accessory = block.get('accessory')
            if accessory is not None and type(accessory) is not dict:
                raise ValueError('accessory_must_be_local_object')
            if accessory and accessory.get('type') == 'button':
                found.append((f'/blocks/{index}/accessory', index, accessory))
    # Find button-shaped objects outside the two qualified locations rather
    # than silently treating a partial inventory as a complete message audit.
    known = {path for path, _, _ in found}
    def walk(value, path):
        if type(value) is dict:
            if value.get('type') == 'button' and path not in known:
                raise ValueError('button_outside_local_supported_locations')
            for key, child in value.items():
                walk(child, path+'/'+str(key))
        elif type(value) is list:
            for number, child in enumerate(value):
                walk(child, path+'/'+str(number))
    walk(message, '')
    return found


def predict_route(button, slackbot_available):
    if slackbot_available is not None and type(slackbot_available) is not bool:
        raise ValueError('user_availability_must_be_bool_or_unknown')
    prompt_present = 'agent_prompt' in button
    if prompt_present and (type(button['agent_prompt']) is not str or not button['agent_prompt'].strip() or len(button['agent_prompt']) > 4000):
        return {'qualified': False, 'routes': [], 'reason': 'prompt_outside_local_profile'}
    if prompt_present and 'url' in button:
        return {'qualified': False, 'routes': [], 'reason': 'mixed_url_and_prompt_not_qualified'}
    if not prompt_present:
        return {'qualified': True, 'routes': ['app_interaction'], 'reason': 'ordinary_button_path'}
    if slackbot_available is None:
        routes = ['app_interaction', 'slackbot_handoff']
    else:
        routes = ['slackbot_handoff' if slackbot_available else 'app_interaction']
    return {'qualified': True, 'routes': routes, 'reason': 'agent_prompt_user_dependent_path'}


def audit(message, roles, slackbot_available, inventory_qualified=True):
    if type(roles) is not dict or type(inventory_qualified) is not bool:
        raise ValueError('outside_local_audit_contract')
    if slackbot_available is not None and type(slackbot_available) is not bool:
        raise ValueError('user_availability_must_be_bool_or_unknown')
    located = button_locations(message)
    known = {path for path, _, _ in located}
    if set(roles)-known:
        raise ValueError('role_points_to_missing_button')
    decisions = []
    identity_counts = Counter((block, button.get('action_id')) for _, block, button in located if text_id(button.get('action_id')))
    for path, block, button in located:
        role = roles.get(path, 'unqualified')
        if role not in ('erp_command', 'assistance', 'unqualified'):
            raise ValueError('unknown_local_button_role')
        action = button.get('action_id')
        duplicate = text_id(action) and identity_counts[(block, action)] > 1
        route = predict_route(button, slackbot_available)
        reason = None
        if not inventory_qualified:
            reason = 'inventory_not_qualified'
        elif not route['qualified']:
            reason = route['reason']
        elif role == 'unqualified':
            reason = 'button_role_not_qualified'
        elif not text_id(action):
            reason = 'local_handler_identity_missing'
        elif duplicate:
            reason = 'action_identity_repeated_within_block'
        elif role == 'erp_command' and route['routes'] != ['app_interaction']:
            reason = 'app_delivery_not_guaranteed_for_supplied_user_profile'
        decisions.append({'local_path': path, 'local_role': role,
                          'predicted_routes_assuming_click': route['routes'],
                          'assessment': 'needs_review' if reason else 'route_matches_local_role',
                          'reason': reason,
                          'orphan_prompt_display': 'agent_prompt_display' in button and 'agent_prompt' not in button,
                          'native_click_observed': False,
                          'erp_authorized_or_completed': False})
    return {'buttons': decisions, 'inventory_qualified': inventory_qualified,
            'native_schema_or_permissions_certified': False}


def run_checks():
    from slack_sdk.models.blocks import ButtonElement, ActionsBlock, SectionBlock
    from slack_sdk.version import __version__
    if __version__ != PINNED_SDK_VERSION:
        raise RuntimeError('Exercise requires slack-sdk=='+PINNED_SDK_VERSION)
    names = []
    def check(name, condition):
        if name in names or not condition:
            raise AssertionError(name)
        names.append(name)
    button = {'type': 'button', 'text': {'type': 'plain_text', 'text': 'Préparer le dossier fictif'},
              'action_id': 'erp_prepare', 'value': 'ORDER-FICTIVE-17',
              'agent_prompt': 'Expliquer ce dossier fictif sans effectuer de modification.',
              'agent_prompt_display': 'Comprendre le dossier'}
    before = copy.deepcopy(button)
    stream = io.StringIO()
    handler = logging.StreamHandler(stream)
    logger = logging.getLogger('slack_sdk.models')
    old_level = logger.level
    logger.setLevel(logging.DEBUG)
    logger.addHandler(handler)
    try:
        direct = ButtonElement(**copy.deepcopy(button)).to_dict()
        actions = ActionsBlock(block_id='erp-actions', elements=[copy.deepcopy(button)]).to_dict()
        section = SectionBlock(block_id='erp-context', text={'type': 'mrkdwn', 'text': 'Dossier fictif'}, accessory=copy.deepcopy(button)).to_dict()
    finally:
        logger.removeHandler(handler)
        logger.setLevel(old_level)
    sdk_buttons = {'direct': direct, 'actions': actions['elements'][0], 'section': section['accessory']}
    for origin, actual in sdk_buttons.items():
        check('sdk_'+origin+'_omits_agent_prompt', 'agent_prompt' not in actual)
        check('sdk_'+origin+'_omits_prompt_display', 'agent_prompt_display' not in actual)
        check('sdk_'+origin+'_retains_handler_and_value', actual['action_id'] == button['action_id'] and actual['value'] == button['value'])
    check('original_fixture_preserved_by_copies', button == before)
    check('sdk_debug_diagnostic_identifies_ignored_fields', 'agent_prompt' in stream.getvalue() and 'ignored' in stream.getvalue())
    raw_message = {'blocks': [{'type': 'actions', 'block_id': 'erp-actions', 'elements': [button]}]}
    plain = json.loads(json.dumps(raw_message, ensure_ascii=False))
    check('json_roundtrip_preserves_both_agent_fields', plain['blocks'][0]['elements'][0] == button)
    erp_role = {'/blocks/0/elements/0': 'erp_command'}
    assistance_role = {'/blocks/0/elements/0': 'assistance'}
    yes = audit(plain, erp_role, True)
    no = audit(plain, erp_role, False)
    unknown = audit(plain, erp_role, None)
    check('prompt_with_available_slackbot_predicts_handoff', yes['buttons'][0]['predicted_routes_assuming_click'] == ['slackbot_handoff'])
    check('handoff_does_not_guarantee_business_callback', yes['buttons'][0]['assessment'] == 'needs_review')
    check('without_slackbot_predicts_app_fallback', no['buttons'][0]['predicted_routes_assuming_click'] == ['app_interaction'])
    check('fallback_can_match_local_command_route', no['buttons'][0]['assessment'] == 'route_matches_local_role')
    check('unknown_availability_keeps_both_paths', set(unknown['buttons'][0]['predicted_routes_assuming_click']) == {'app_interaction','slackbot_handoff'})
    check('unknown_availability_does_not_clear_erp_route', unknown['buttons'][0]['assessment'] == 'needs_review')
    for availability in (True, False, None):
        check('assistance_routes_remain_explicit_'+str(availability), audit(plain, assistance_role, availability)['buttons'][0]['assessment'] == 'route_matches_local_role')
        sdk_message = {'blocks': [{'type': 'actions', 'elements': [direct]}]}
        value = audit(sdk_message, erp_role, availability)
        check('serialized_sdk_missing_prompt_predicts_app_'+str(availability), value['buttons'][0]['predicted_routes_assuming_click'] == ['app_interaction'])
    # A link button is a negative control. The consulted contract still sends
    # an interaction for url without agent_prompt; do not call every link a handoff.
    link = copy.deepcopy(direct)
    link['url'] = 'https://example.com/fictional-order'
    check('ordinary_link_button_still_predicts_app_interaction', predict_route(link, True)['routes'] == ['app_interaction'])
    mixed = copy.deepcopy(button)
    mixed['url'] = link['url']
    check('undocumented_mixed_precedence_is_not_guessed', not predict_route(mixed, True)['qualified'])
    check('orphan_display_does_not_activate_handoff', predict_route({**direct,'agent_prompt_display':'Résumé'},True)['routes'] == ['app_interaction'])
    check('orphan_display_is_reported', audit({'blocks':[{'type':'actions','elements':[{**direct,'agent_prompt_display':'Résumé'}]}]},erp_role,True)['buttons'][0]['orphan_prompt_display'])
    check('role_is_not_inferred_from_green_style', audit({'blocks':[{'type':'actions','elements':[{**direct,'style':'primary'}]}]}, {}, True)['buttons'][0]['reason'] == 'button_role_not_qualified')
    check('handler_absence_is_local_gap_not_permission', audit({'blocks':[{'type':'actions','elements':[{k:v for k,v in direct.items() if k!='action_id'}]}]},erp_role,True)['buttons'][0]['reason'] == 'local_handler_identity_missing')
    check('incomplete_inventory_is_not_cleared', audit(plain,erp_role,False,False)['buttons'][0]['reason']=='inventory_not_qualified')
    duplicate = {'blocks':[{'type':'actions','elements':[direct,copy.deepcopy(direct)]}]}
    dupe = audit(duplicate,{'/blocks/0/elements/0':'erp_command','/blocks/0/elements/1':'erp_command'},True)
    check('repeated_handler_identity_within_block_is_reported',dupe['buttons'][1]['reason']=='action_identity_repeated_within_block')
    check('both_buttons_with_ambiguous_identity_are_reported',all(x['reason']=='action_identity_repeated_within_block' for x in dupe['buttons']))
    separate = {'blocks':[{'type':'actions','elements':[direct]},{'type':'section','accessory':copy.deepcopy(direct)}]}
    sep = audit(separate,{'/blocks/0/elements/0':'erp_command','/blocks/1/accessory':'erp_command'},True)
    check('same_action_id_in_distinct_blocks_not_globally_rejected',all(x['assessment']=='route_matches_local_role' for x in sep['buttons']))
    for n, prompt in enumerate(('', None, True, 'x'*4001),1):
        check('unqualified_prompt_'+str(n), not predict_route({**direct,'agent_prompt':prompt},True)['qualified'])
    invalid = [(plain,erp_role,1,True), (plain,erp_role,True,1),
               ({'blocks':[]},{},1,True),
               ({'blocks':{}},erp_role,True,True),
               ({'blocks':[{'type':'actions','elements':{}}]},erp_role,True,True),
               ({'blocks':[{'type':'section','accessory':[]} ]},erp_role,True,True),
               ({'blocks':[{'type':'unsupported_local_block','button':direct}]},{},True,True),
               (plain,{'/blocks/7/accessory':'erp_command'},True,True),
               (plain,{'/blocks/0/elements/0':'unknown-role'},True,True)]
    for n,(message,roles,available,qualified) in enumerate(invalid,1):
        rejected=False
        try:audit(message,roles,available,qualified)
        except ValueError:rejected=True
        check('invalid_local_audit_'+str(n),rejected)
    check('input_message_unchanged_after_audits', raw_message==plain and button==before)
    check('no_predicted_route_certifies_erp_result', all(not x['erp_authorized_or_completed'] for r in (yes,no,unknown,sep) for x in r['buttons']))
    return {'checks_passed':len(names),'check_names':names,'python_version':sys.version,'actual_sdk_version':__version__,
            'actual_sdk_serialization':sdk_buttons,'actual_sdk_debug_reports_ignored_fields':True,
            'examples':{'available_user':yes,'unavailable_user':no,'unknown_user':unknown},
            'scope':{'http_calls':0,'slack_api_calls':0,'erp_operations':0,'persistent_writes_by_exercise':0,
                     'actual_sdk_serialization_executed':True,'native_click_executed':False,
                     'native_user_entitlement_certified':False,'native_api_schema_certified':False,
                     'route_predictions_follow_consulted_contract_not_real_delivery':True}}


if __name__ == '__main__':
    with refuse_connections() as probe:
        output = run_checks()
    assert probe['attempts'] == 0
    output['scope']['connection_attempts_observed'] = probe['attempts']
    print(json.dumps(output,ensure_ascii=False,indent=2))
