from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from event_testing.resolver import Resolver
    from sims.sim_info import SimInfo
    from typing import *
from _collections import defaultdict
from collections import Counter
import operator
from protocolbuffers import UI_pb2, Consts_pb2, Business_pb2, ResourceKey_pb2, DistributorOps_pb2, GameplaySaveData_pb2
from audio.primitive import PlaySound
from business.business_customer_manager import BusinessCustomerManager
from business.business_employee_manager import BusinessEmployeeManager
from business.business_enums import BusinessQualityType, FirstTimeMessageType, BusinessType, BusinessOriginTelemetryContext, BusinessEmployeeType
from business.business_funds import BusinessFunds
from date_and_time import DateAndTime, DATE_AND_TIME_ZERO
from distributor.ops import GenericProtocolBufferOp
from distributor.rollback import ProtocolBufferRollback
from distributor.system import Distributor
from event_testing.resolver import SingleSimResolver
from event_testing.test_events import TestEvent
from gsi_handlers import business_handlers
from interactions import ParticipantType
from objects import ALL_HIDDEN_REASONS
from retail.retail_balance_transfer_dialog import FundsTransferDialog
from sims4.callback_utils import CallableList
from sims.outfits.outfit_enums import OutfitCategory
from singletons import DEFAULT
from small_business.small_business_debug import update_small_business_reviews_debug_visualizer
from small_business.small_business_employee_manager import SmallBusinessEmployeeManager
from small_business.small_business_tuning import SmallBusinessTunables
from world.premade_lot_status import PremadeLotStatus
from zone_types import ZoneState
import distributor
import services
import sims4.log
import telemetry_helper
TELEMETRY_GROUP_BUSINESS = 'BUSI'
TELEMETRY_HOOK_BUSINESS_PURCHASED = 'BUSP'
TELEMETRY_HOOK_NEW_GAME_BUSINESS_PURCHASED = 'BUNG'
TELEMETRY_HOOK_BUSINESS_SOLD = 'BUSS'
TELEMETRY_HOOK_BUSINESS_UNREGISTERED = 'SOLD'
TELEMETRY_HOOK_NPC_BUSINESS_VISITED = 'BUNS'
TELEMETRY_HOOK_BUSINESS_OPEN = 'BUSO'
TELEMETRY_HOOK_BUSINESS_CLOSED = 'BUSC'
TELEMETRY_HOOK_BUSINESS_INHERIT = 'INHT'
TELEMETRY_HOOK_LENGTH_BUSINESS_OPENED = 'lsto'
TELEMETRY_HOOK_NUM_WORKERS = 'numw'
TELEMETRY_HOOK_AMOUNT_PROFIT = 'prof'
TELEMETRY_HOOK_STAR_RATING = 'star'
TELEMETRY_HOOK_BUSINESS_TYPE = 'btyp'
TELEMETRY_HOOK_OCCUPIED = 'ocpd'
TELEMETRY_HOOK_PURCHASE_SRC = 'srce'
TELEMETRY_HOOK_BUSINESS_LOT_ID = 'hlid'
TELEMETRY_HOOK_BUSINESS_ID = 'bsid'
TELEMETRY_HOOK_VENUE_TYPE = 'venu'
TELEMETRY_HOOK_TARGET_SIM = 'tsim'
business_telemetry_writer = sims4.telemetry.TelemetryWriter(TELEMETRY_GROUP_BUSINESS)
logger = sims4.log.Logger('Business', default_owner='trevor')

class BusinessManager:
    EVENTS = (TestEvent.SimDeathTypeSet,)

    def __init__(self, business_type, sim_id:'int'=None):
        self._business_type = business_type
        self._business_type_data = services.business_service().get_business_tuning_data_for_business_type(business_type)
        self._owner_household_id = None
        self._owner_sim_id = sim_id
        self._zone_id = None
        self._create_employee_manager()
        self._customer_manager = BusinessCustomerManager(self)
        self._is_open = False
        self._open_time = None
        self._total_open_hours = 0
        self._last_off_lot_update = None
        self.on_store_closed = CallableList()
        self._funds_category_tracker = Counter()
        self._funds = None
        self._markup_multiplier = self.tuning_data.default_markup_multiplier
        self._daily_revenue = 0
        self._daily_items_sold = 0
        self._grand_opening = False
        self._star_rating_value = self._business_type_data.default_business_star_rating_value
        self._buff_bucket_totals = defaultdict(float)
        self._buff_bucket_size = 0
        self._summary_dialog_class = None
        self._quality_unlocked = False
        self._off_lot_negative_profit_notification_shown = False
        self.make_unowned_on_load = False
        self._star_rating_to_notification_cooldowns = None
        self._star_rating_to_slam_cooldowns = None

    @property
    def business_type(self):
        return self._business_type

    @property
    def owner_household_id(self):
        return self._owner_household_id

    @property
    def owner_sim_id(self):
        return self._owner_sim_id

    @property
    def is_owned_by_npc(self):
        return self._owner_household_id is None

    @property
    def persisted_npc_owned(self):
        return False

    @property
    def business_zone_id(self):
        return self._zone_id

    @property
    def tuning_data(self):
        return self._business_type_data

    @property
    def is_open(self):
        return self._is_open

    def can_open(self):
        return not self._is_open

    @property
    def minutes_open(self):
        timespan = self.get_timespan_since_open()
        if timespan is None:
            return 0
        return timespan.in_minutes()

    @property
    def allow_super_speed_3(self):
        if self._business_type_data.always_allow_super_speed_3:
            return True
        elif self._is_open:
            return False
        return True

    @property
    def daily_revenue(self):
        return self._daily_revenue

    @property
    def funds(self):
        return self._funds

    @property
    def funds_transfer_gain_reason(self):
        return Consts_pb2.FUNDS_RETAIL_TRANSFER_GAIN

    @property
    def funds_transfer_loss_reason(self):
        return Consts_pb2.FUNDS_RETAIL_TRANSFER_LOSS

    @property
    def daily_items_sold(self):
        return self._daily_items_sold

    @daily_items_sold.setter
    def daily_items_sold(self, value):
        self._daily_items_sold = value
        self._send_daily_items_sold_update()

    @property
    def is_owner_household_active(self):
        if self._owner_household_id is None:
            return False
        return self._owner_household_id == services.active_household_id()

    @property
    def markup_multiplier(self):
        return self._markup_multiplier

    @property
    def employee_count(self):
        return self._employee_manager.employee_count

    @property
    def quality_setting(self):
        return BusinessQualityType.INVALID

    @property
    def has_rules(self) -> 'bool':
        return False

    @property
    def clear_lot_ownership_on_sell(self) -> 'bool':
        return True

    @property
    def clear_lot_ownership_on_death_of_owner(self) -> 'bool':
        return True

    @property
    def disown_household_objects_on_sell(self) -> 'bool':
        return True

    @property
    def include_furniture_price_on_sell(self) -> 'bool':
        return True

    @property
    def occupied(self) -> 'bool':
        return False

    @property
    def total_open_hours(self):
        if self._open_time is None:
            return self._total_open_hours
        else:
            return self._total_open_hours + int((services.game_clock_service().now() - self._open_time).in_hours())

    @property
    def lifetime_customers_served(self):
        if self._customer_manager is None:
            return 0
        return int(self._customer_manager.lifetime_customers_served)

    def set_owner_household_id(self, owner_household_id):
        self._owner_household_id = owner_household_id
        starting_funds = self.tuning_data.npc_starting_funds if self._owner_household_id is None else 0
        self._funds = BusinessFunds(self._owner_household_id, starting_funds, self)
        self._grand_opening = self._owner_household_id is not None

    def set_zone_id(self, business_zone_id):
        self._zone_id = business_zone_id

    def get_star_rating(self):
        star_rating = self._get_star_rating_from_curve()
        star_rating = round(star_rating*2)/2
        return sims4.math.clamp(self.tuning_data.min_and_max_star_rating.lower_bound, star_rating, self.tuning_data.min_and_max_star_rating.upper_bound)

    def is_household_owner(self, household_id):
        if self._owner_household_id is None:
            return False
        return self._owner_household_id == household_id

    def is_matching_employee_type(self, employee_type:'BusinessEmployeeType') -> 'bool':
        return employee_type in self._business_type_data.employee_data_map.keys()

    def should_automatically_close(self):
        return False

    def add_owner_career(self):
        pass

    def _update_off_lot_time_and_get_delta(self):
        now = services.time_service().sim_now
        hours_since_last_sim = (now - self._last_off_lot_update).in_hours()
        self._last_off_lot_update = now
        return hours_since_last_sim

    def run_off_lot_simulation(self):
        if self.should_show_no_way_to_make_money_notification():
            return
        if self._last_off_lot_update is None:
            return
        if not self.tuning_data.off_lot_star_rating_increase_per_hour_curve:
            return
        hours_since_last_sim = self._update_off_lot_time_and_get_delta()
        star_rating = self.get_star_rating()
        value_change = 0
        if sims4.random.random_chance(self.tuning_data.off_lot_chance_of_star_rating_increase):
            value_change = self.tuning_data.off_lot_star_rating_increase_per_hour_curve.get(star_rating)
            if business_handlers.business_archiver.enabled:
                business_handlers.archive_business_event('OffLot', None, 'business with star rating: {} using increase mapping with value change of: {}'.format(star_rating, value_change))
        else:
            value_change = self.tuning_data.off_lot_star_rating_decay_per_hour_curve.get(star_rating)
            perk_tuning = self.tuning_data.off_lot_star_rating_decay_multiplier_perk
            perk_unlocked = False
            if perk_tuning is not None:
                bucks_tracker = services.active_household().bucks_tracker
                if bucks_tracker.is_perk_unlocked(perk_tuning.perk):
                    value_change *= perk_tuning.decay_multiplier
                    perk_unlocked = True
            if business_handlers.business_archiver.enabled:
                if perk_unlocked:
                    event_description = 'business with star rating: {} using decay mapping with value change of: {} after multiplier applied :{}'.format(star_rating, value_change, perk_tuning.decay_multiplier)
                else:
                    event_description = 'business with star rating: {} using decay mapping with value change of: {}'.format(star_rating, value_change)
                business_handlers.archive_business_event('OffLot', None, event_description)
        value_change *= hours_since_last_sim
        self._adjust_star_rating_value(value_change, send_update_message=False)
        self._adjust_profit(hours_since_last_sim)
        self._send_review_update_message()
        self.send_daily_profit_and_cost_update()

    def prepare_for_off_lot_simulation(self):
        self._off_lot_negative_profit_notification_shown = False

    def _adjust_profit(self, hours_since_last_sim):
        final_customer_count = self._get_off_lot_customer_count(hours_since_last_sim)
        self._customer_manager._session_customers_served += final_customer_count
        self._customer_manager._lifetime_customers_served += final_customer_count
        self._customer_manager._send_daily_customers_served_update()
        profit_per_service = self._get_average_profit_per_service()
        profit_per_service *= self.tuning_data.off_lot_profit_per_item_multiplier
        total_profit = profit_per_service*final_customer_count
        self.modify_funds(total_profit, from_item_sold=True)
        if self.tuning_data.off_lot_net_loss_notification is None:
            return
        employee_wages = self._employee_manager.get_total_employee_wages_per_hour()*hours_since_last_sim
        advertising_cost = self._advertising_manager.get_advertising_cost_per_hour()*hours_since_last_sim
        net_profit = total_profit - employee_wages - advertising_cost
        if not self._off_lot_negative_profit_notification_shown:
            zone_data = services.get_persistence_service().get_zone_proto_buff(self._zone_id)
            active_sim_info = services.active_sim_info()
            if active_sim_info is not None:
                notification = self.tuning_data.off_lot_net_loss_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
                notification.show_dialog(additional_tokens=(zone_data.name,))
                self._off_lot_negative_profit_notification_shown = True

    def set_open(self, is_open, is_npc_business=False):
        if self._is_open == is_open:
            return
        if is_open:
            self._open_business(is_npc_business=is_npc_business)
        else:
            self._close_business()

    def start_already_opened_business(self):
        self._employee_manager.open_business()
        self.tuning_data.lighting_helper_open.execute_lighting_helper(self)

    def should_close_after_load(self):
        current_zone = services.current_zone()
        if current_zone.active_household_changed_between_save_and_load() and services.active_household_id() == self._owner_household_id:
            return True
        if current_zone.id == self._zone_id:
            return self.should_automatically_close() and current_zone.time_has_passed_in_world_since_zone_save()
        return self.should_automatically_close()

    def is_employee(self, sim_info):
        return self._employee_manager.is_employee(sim_info)

    def is_valid_employee_type(self, employee_type):
        return employee_type in self.tuning_data.employee_data_map

    def is_valid_employee_skill(self, employee_skill, employee_type):
        employee_type_data = self.tuning_data.employee_data_map.get(employee_type, None)
        if employee_type_data is None:
            return False
        return employee_skill in employee_type_data.employee_skills

    def get_employee_count(self, employee_type):
        return self._employee_manager.get_number_of_employees_by_type(employee_type)

    def get_employees_by_type(self, employee_type):
        return self._employee_manager.get_employees_by_type(employee_type)

    def get_employee_data(self, sim_info):
        return self._employee_manager.get_employee_data(sim_info)

    def remove_employee(self, sim_info, is_quitting:'bool'=True):
        self._employee_manager.remove_employee(sim_info, is_quitting)
        self.send_min_employee_req_met_update_message()
        if self.should_show_no_way_to_make_money_notification():
            active_sim_info = services.active_sim_info()
            notification = self.tuning_data.no_way_to_make_money_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
            notification.show_dialog()

    def get_total_employee_wages_per_hour(self):
        return self._employee_manager.get_total_employee_wages_per_hour()

    def get_desired_career_level(self, sim_info, employee_type):
        return self._employee_manager.get_desired_career_level(sim_info, employee_type)

    def get_customer_star_rating(self, sim_id):
        return self._customer_manager.get_customer_star_rating(sim_id)

    def get_customer_bucket_totals(self, sim_id):
        return self._customer_manager.get_customer_bucket_totals(sim_id)

    def get_business_bucket_totals(self):
        return self._buff_bucket_totals

    def _should_make_customer(self, sim_info):
        return self.is_owner_household_active and not self.is_household_owner(sim_info.household_id)

    def add_customer(self, sim_info):
        if self._should_make_customer(sim_info):
            self._customer_manager.add_customer(sim_info.sim_id)

    def remove_customer(self, sim_info, review_business=True):
        if not self._should_make_customer(sim_info):
            return
        self._customer_manager.remove_customer(sim_info, review_business)

    def set_daily_revenue(self, value):
        self._daily_revenue = value
        if self._owner_household_id is not None:
            self.send_daily_profit_and_cost_update()

    def set_markup_multiplier(self, multiplier):
        valid_multipliers = [entry.markup_multiplier for entry in self._business_type_data.markup_multiplier_data]
        if multiplier in valid_multipliers:
            self._markup_multiplier = multiplier
            self.send_markup_multiplier_message()
            self._distribute_markup_multiplier_update()
        else:
            logger.error('Tried setting the markup multiplier to an invalid multiplier. Invalid multiplier is: {}. Valid multipliers are: {}.', multiplier, valid_multipliers)

    def _distribute_markup_multiplier_update(self):
        markup_msg = Business_pb2.BusinessMarkupUpdate()
        if self.business_zone_id is not None:
            markup_msg.zone_id = self.business_zone_id
        if self.owner_sim_id is not None:
            markup_msg.sim_id = self.owner_sim_id
        markup_msg.markup_chosen = self.markup_multiplier
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_MARKUP_DATA_UPDATE, markup_msg)
        Distributor.instance().add_op_with_no_owner(op)

    def get_value_with_markup(self, value) -> 'int':
        markup_multiplier = self.markup_multiplier
        if self.owner_household_id is not None:
            tracker = services.business_service().get_business_tracker_for_household(self.owner_household_id, self.business_type)
            markup_multiplier += tracker.additional_markup_multiplier
        else:
            active_household = services.active_household()
            if active_household is not None:
                markup_multiplier *= active_household.holiday_tracker.get_active_holiday_business_price_multiplier(self.business_type)
        return int(value*markup_multiplier)

    def add_to_funds_category(self, funds_category, amount):
        if not self.is_open:
            return
        self._funds_category_tracker[funds_category] += amount
        self.send_daily_profit_and_cost_update()

    def get_funds_category_entries_gen(self):
        for (funds_category, amount) in self._funds_category_tracker.items():
            funds_category_data = self.tuning_data.funds_category_data.get(funds_category)
            if funds_category_data is not None and funds_category_data.summary_dialog_entry is not None:
                yield (funds_category_data.summary_dialog_entry, amount)

    def potential_manage_outfit_interactions_gen(self, context, **kwargs):
        for affordance in self.tuning_data.manage_outfit_affordances:
            for aop in affordance.potential_interactions(context.sim, context, **kwargs):
                yield aop

    def add_unowned_business_on_sell(self) -> 'BusinessType':
        return BusinessType.INVALID

    def sell_business_finalize_funds(self, lot_value:'int'=0, lot_sold:'bool'=True):
        self.modify_funds(lot_value)
        self.transfer_balance_to_household()

    def get_sell_store_dialog(self):
        return self.tuning_data.sell_store_dialog(services.get_zone(self.business_zone_id))

    def modify_funds(self, amount, from_item_sold=True, from_comped_item=False, funds_category=None, employee_wages:'bool'=False):
        if amount == 0:
            return
        if amount < 0:
            if from_item_sold:
                logger.warn("Trying to deduct funds from a business but claiming it's from an item sold. Makes no sense so we're bailing.")
                return
            self._funds.try_remove(-amount, Consts_pb2.FUNDS_RETAIL_PROFITS, funds_category=funds_category)
        else:
            if from_comped_item:
                logger.warn("Trying to add funds to a business but claiming it's from a comped meal. Comped meals should deduct money, not add.")
            self._funds.add(amount, Consts_pb2.FUNDS_RETAIL_PROFITS)
        if from_item_sold or from_comped_item:
            self.set_daily_revenue(self.daily_revenue + amount)
        if from_item_sold:
            self.daily_items_sold += 1

    def transfer_balance_to_household(self):
        owner_household = services.household_manager().get(self._owner_household_id)
        if owner_household is None:
            return
        sim = next(owner_household.instanced_sims_gen(allow_hidden_flags=ALL_HIDDEN_REASONS))
        funds = self._funds.money
        if funds > 0:
            owner_household._funds.add(funds, Consts_pb2.FUNDS_RETAIL_PROFITS, sim)
        self._funds.try_remove(funds, Consts_pb2.FUNDS_RETAIL_PROFITS, sim)

    def get_daily_outgoing_costs(self, include_employee_wages=True):
        employee_wages = self._employee_manager._daily_employee_wages if include_employee_wages else 0
        return int(employee_wages + sum(self._funds_category_tracker.values()))

    def get_cost_from_tracker(self, funds_category):
        return self._funds_category_tracker.get(funds_category, 0)

    def get_daily_net_profit(self, **kwargs):
        return int(self.daily_revenue - self.get_daily_outgoing_costs(**kwargs))

    def get_skills_for_employee_type(self, employee_type):
        employee_type_tuning_data = self._get_tuning_data_for_employee_type(employee_type)
        if employee_type_tuning_data is None:
            return
        return employee_type_tuning_data.skills

    def get_uniform_pose_for_employee_type(self, employee_type):
        employee_type_tuning_data = self._get_tuning_data_for_employee_type(employee_type)
        if employee_type_tuning_data is None:
            return
        return employee_type_tuning_data.uniform_pose

    def is_employee_clocked_in(self, sim_info):
        return self._employee_manager.is_employee_clocked_in(sim_info)

    def on_employee_clock_in(self, employee_sim_info):
        self._employee_manager.on_employee_clock_in(employee_sim_info)

    def on_employee_clock_out(self, employee_sim_info, career_level=DEFAULT):
        self._employee_manager.on_employee_clock_out(employee_sim_info, career_level=career_level)

    def update_employees(self):
        self._employee_manager.update_employees()

    def get_employee_uniform_data(self, employee_type, gender, sim_id=0):
        return self._employee_manager.get_employee_uniform_data(employee_type, gender, OutfitCategory.CAREER, sim_id)

    def set_quality_setting(self, quality):
        raise NotImplementedError('set_quality is not implemented this means this business is using the base business manager, no functionality is implemented')

    def set_advertising_type(self, advertising_type):
        raise NotImplementedError('set_advertising_type is not implemented this means this business is using the base business manager, no functionality is implemented')

    def get_current_advertising_cost(self):
        raise NotImplementedError('get_current_advertising_cost is not implemented this means this business is using the base business manager, no functionality is implemented')

    def get_advertising_type_for_gsi(self):
        return ''

    def get_total_employee_wages(self):
        if not self.is_open:
            return self._employee_manager.final_daily_wages()
        return self._employee_manager.get_total_employee_wages()

    def get_employee_career_level(self, employee_sim_info):
        return self._employee_manager.get_employee_career_level(employee_sim_info)

    def get_employee_career(self, employee_sim_info):
        return self._employee_manager.get_employee_career(employee_sim_info)

    def add_employee(self, sim_info, employee_type, is_npc_employee=False):
        self._employee_manager.add_employee(sim_info, employee_type, is_npc_employee=is_npc_employee)
        self.send_min_employee_req_met_update_message()

    def get_employees_on_payroll(self):
        return self._employee_manager.get_employees_on_payroll()

    def get_employee_wages(self, employee_sim_info):
        return self._employee_manager.get_employee_wages(employee_sim_info)

    def get_employee_wages_breakdown_gen(self, employee_sim_info):
        return self._employee_manager.get_employee_wages_breakdown_gen(employee_sim_info)

    def get_outfit_index_for_employee(self, employee_sim_info) -> 'int':
        return self._employee_manager.get_outfit_index_for_employee(employee_sim_info)

    def run_hire_interaction(self, target_sim, employee_type):
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_hire, target_sim)

    def run_fire_employee_interaction(self, target_sim):
        employee_data = self._employee_manager.get_employee_data(target_sim.sim_info)
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_data.employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_fire, target_sim)

    def run_promote_employee_interaction(self, target_sim):
        employee_data = self._employee_manager.get_employee_data(target_sim.sim_info)
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_data.employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_promote, target_sim)

    def run_demote_employee_interaction(self, target_sim):
        employee_data = self._employee_manager.get_employee_data(target_sim.sim_info)
        employee_tuning_data = self._employee_manager.get_employee_tuning_data_for_employee_type(employee_data.employee_type)
        return self._employee_manager.run_employee_interaction(employee_tuning_data.interaction_demote, target_sim)

    def try_open_npc_store(self):
        if self.owner_household_id is None:
            self._open_pure_npc_store(services.active_lot().get_premade_status() == PremadeLotStatus.IS_PREMADE)
        elif not self.is_owner_household_active:
            self._open_household_owned_npc_store()
        with telemetry_helper.begin_hook(business_telemetry_writer, TELEMETRY_HOOK_NPC_BUSINESS_VISITED, household=services.household_manager().get(self.owner_household_id)) as hook:
            hook.write_enum(TELEMETRY_HOOK_BUSINESS_TYPE, self.business_type)

    def send_data_to_client(self):
        zone = services.get_zone(self._zone_id, allow_uninstantiated_zones=True) if self._zone_id is not None else None
        if zone is None and self.owner_sim_id is None:
            logger.error('Trying to send the business data to client but the business manager {} has an invalid zone id and sim_id.', self)
            return
        business_data_msg = Business_pb2.SetBusinessData()
        self.construct_business_message(business_data_msg)
        business_data_op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.SET_BUSINESS_DATA, business_data_msg)
        Distributor.instance().add_op_with_no_owner(business_data_op)

    def construct_business_message(self, msg):
        msg.zone_id = self.business_zone_id
        persistence = services.get_persistence_service()
        zone_data = persistence.get_zone_proto_buff(self.business_zone_id)
        if zone_data is not None:
            msg.name = zone_data.name
        msg.is_open = self.is_open
        if self._open_time is not None:
            msg.time_opened = self._open_time.absolute_ticks()
        msg.daily_items_sold = self._daily_items_sold
        msg.daily_outgoing_costs = self.get_daily_outgoing_costs(include_employee_wages=False)
        msg.funds = self.funds.money
        msg.daily_customers_served = self._customer_manager.session_customers_served
        msg.net_profit = self.get_daily_net_profit(include_employee_wages=False)
        msg.markup_chosen = self.markup_multiplier
        msg.daily_revenue = int(self._daily_revenue)
        icon_tuning = self.tuning_data.business_icon
        msg.icon = ResourceKey_pb2.ResourceKey()
        msg.icon.instance = icon_tuning.instance
        msg.icon.group = icon_tuning.group
        msg.icon.type = icon_tuning.type
        msg.review_data = Business_pb2.ReviewDataUpdate()
        self._populate_review_update_message(msg.review_data)
        msg.minimum_employee_requirements_met = self.meets_minimum_employee_requirment()

    def get_interpolated_buff_bucket_value(self, buff_bucket_type, value):
        bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data.get(buff_bucket_type)
        bucket_total = sims4.math.clamp(-1, value, 1)
        if bucket_total >= 0:
            return sims4.math.interpolate(bucket_data.bucket_value_median, bucket_data.bucket_value_maximum, bucket_total)
        return sims4.math.interpolate(bucket_data.bucket_value_median, bucket_data.bucket_value_minimum, abs(bucket_total))

    def process_customer_rating(self, customer_sim_info, customer_star_rating, customer_buff_bucket_totals):
        if self.tuning_data.customer_rating_delta_to_business_star_rating_value_change_curve is not None:
            star_rating_delta = customer_star_rating - self.get_star_rating()
            star_rating_value_change = self.tuning_data.customer_rating_delta_to_business_star_rating_value_change_curve.get(star_rating_delta)
            critic_tuning = self.tuning_data.critic
            if customer_sim_info.has_trait(critic_tuning.critic_trait):
                star_rating_value_change *= critic_tuning.critic_star_rating_application_count
            for (bucket, value) in customer_buff_bucket_totals.items():
                self._buff_bucket_totals[bucket] += self.get_interpolated_buff_bucket_value(bucket, value)
            self._buff_bucket_size += 1
            self._adjust_star_rating_value(star_rating_value_change)
        loot_action = self.tuning_data.customer_star_rating_leaving_loot_actions.get(customer_star_rating)
        if loot_action is not None:
            owner_sim_info = services.sim_info_manager().get(self.owner_sim_id)
            resolver = SingleSimResolver(owner_sim_info)
            loot_action.apply_to_resolver(resolver)

    def show_summary_dialog(self, is_from_close=False):
        if self._summary_dialog_class is None:
            raise NotImplementedError('No Summary Dialog specified for business {}'.format(self.business_type()))
        if services.active_household() is None:
            return
        self._summary_dialog_class(self).show_business_summary_dialog()

    def send_min_employee_req_met_update_message(self):
        update_message = Business_pb2.MinEmployeeReqMetUpdate()
        update_message.zone_id = services.current_zone_id() if self.business_zone_id == None else self.business_zone_id
        update_message.minimum_employee_requirements_met = self.meets_minimum_employee_requirment()
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_EMPLOYEE_MIN_REQUIREMENT_UPDATE, update_message)
        Distributor.instance().add_op_with_no_owner(op)

    def send_telemetry_origin_message(self, telemetry_context:'BusinessOriginTelemetryContext'=BusinessOriginTelemetryContext.NONE) -> 'None':
        zone_data = services.get_persistence_service().get_zone_proto_buff(self.business_zone_id)
        lot_description_id = zone_data.lot_description_id if zone_data is not None else 0
        with telemetry_helper.begin_hook(business_telemetry_writer, TELEMETRY_HOOK_BUSINESS_PURCHASED, household=services.household_manager().get(self.owner_household_id)) as hook:
            hook.write_enum(TELEMETRY_HOOK_BUSINESS_TYPE, self.business_type)
            hook.write_enum(TELEMETRY_HOOK_PURCHASE_SRC, telemetry_context)
            hook.write_bool(TELEMETRY_HOOK_OCCUPIED, self.occupied)
            if self.business_type == BusinessType.SMALL_BUSINESS:
                hook.write_int(TELEMETRY_HOOK_BUSINESS_ID, self.owner_sim_id)
                venue_type = services.venue_service().active_venue.venue_type
                hook.write_enum(TELEMETRY_HOOK_VENUE_TYPE, venue_type)
            else:
                hook.write_int(TELEMETRY_HOOK_BUSINESS_LOT_ID, lot_description_id)

    def _create_employee_manager(self):
        self._employee_manager = BusinessEmployeeManager(self)

    def _open_household_owned_npc_store(self):
        self.set_open(True, is_npc_business=True)

    def _get_star_rating_from_curve(self):
        curve_data = self.tuning_data.star_rating_value_to_user_facing_rating_curve
        if curve_data is None:
            return 0
        return curve_data.get(self._star_rating_value)

    def get_number_of_customer_processed(self) -> 'int':
        return self._buff_bucket_size

    def get_sorted_delta_buff_buckets_tuple(self):
        if self._buff_bucket_size == 0:
            return
        difference = defaultdict(float)
        for (bucket, value) in self._buff_bucket_totals.items():
            buff_bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[bucket]
            if buff_bucket_data.bucket_hidden_review:
                pass
            else:
                delta = buff_bucket_data.bucket_value_maximum - value/self._buff_bucket_size
                if delta < buff_bucket_data.bucket_growth_opportunity_threshold:
                    pass
                else:
                    difference[bucket] = delta
        return sorted(difference.items(), key=operator.itemgetter(1), reverse=True)

    def get_top_performing_bucket_tuple(self):
        if self._buff_bucket_size == 0:
            return (None, None)
        top_value = None
        top_bucket = None
        for (bucket, value) in self._buff_bucket_totals.items():
            value /= self._buff_bucket_size
            if not top_value is None:
                if value > top_value:
                    bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[bucket]
                    if bucket_data.bucket_hidden_review:
                        pass
                    elif value > bucket_data.bucket_excellence_threshold:
                        top_value = value
                        top_bucket = bucket
            bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[bucket]
            if bucket_data.bucket_hidden_review:
                pass
            elif value > bucket_data.bucket_excellence_threshold:
                top_value = value
                top_bucket = bucket
        return (top_bucket, top_value)

    def set_star_rating_value(self, value):
        previous_whole_star_rating = int(self.get_star_rating())
        self._star_rating_value = 0
        self._adjust_star_rating_value(value, previous_whole_star_rating=previous_whole_star_rating)

    def get_resolver(self, actor:'Optional[SimInfo]'=DEFAULT) -> 'Resolver':
        if actor is DEFAULT:
            actor = services.active_sim_info()
        actor_household = services.household_manager().get(self._owner_household_id)
        additional_participants = {ParticipantType.PickedZoneId: (self._zone_id,), ParticipantType.ActorHousehold: (actor_household,)}
        return SingleSimResolver(actor, additional_participants=additional_participants)

    def _adjust_star_rating_value(self, delta:'float', send_update_message:'bool'=True, previous_whole_star_rating:'int'=None) -> 'None':
        if previous_whole_star_rating is None:
            previous_whole_star_rating = int(self.get_star_rating())
        self._star_rating_value = sims4.math.clamp(self.tuning_data.min_and_max_star_rating_value.lower_bound, self._star_rating_value + delta, self.tuning_data.min_and_max_star_rating_value.upper_bound)
        current_whole_star_rating = int(self.get_star_rating())
        if delta != 0.0:
            self.on_rating_change(previous_whole_star_rating, current_whole_star_rating)
        if send_update_message:
            self._send_review_update_message()

    def on_rating_change(self, previous_whole_star_rating:'int', current_whole_star_rating:'int', rating_delta:'Optional[int]'=None) -> 'None':
        if self.is_owner_household_active:
            services.business_service().show_first_time_dialog(self, FirstTimeMessageType.ANY_RATING_CHANGE)
        if previous_whole_star_rating == current_whole_star_rating:
            return
        sim_now = services.time_service().sim_now
        active_sim_info = services.active_sim_info()
        if self.is_owner_household_active:
            services.business_service().show_first_time_dialog(self, FirstTimeMessageType.STAR_RATING_CHANGE)
            if current_whole_star_rating in self.tuning_data.star_rating_to_notification_map:
                if self._star_rating_to_notification_cooldowns is None:
                    self._star_rating_to_notification_cooldowns = {}
                if sim_now > self._star_rating_to_notification_cooldowns.get(current_whole_star_rating, DATE_AND_TIME_ZERO):
                    notification_factory = self.tuning_data.star_rating_to_notification_map.get(current_whole_star_rating)
                    dialog = notification_factory(active_sim_info, resolver=self.get_resolver())
                    dialog.show_dialog()
                    new_cooldown = sim_now + self.tuning_data.star_rating_notification_cooldown()
                    self._star_rating_to_notification_cooldowns[current_whole_star_rating] = new_cooldown
        if current_whole_star_rating in self.tuning_data.star_rating_to_screen_slam_map:
            if self._star_rating_to_slam_cooldowns is None:
                self._star_rating_to_slam_cooldowns = {}
            if sim_now > self._star_rating_to_slam_cooldowns.get(current_whole_star_rating, DATE_AND_TIME_ZERO):
                screen_slam = self.tuning_data.star_rating_to_screen_slam_map.get(current_whole_star_rating)
                screen_slam.send_screen_slam_message(active_sim_info)
                new_cooldown = sim_now + self.tuning_data.star_rating_screen_slam_cooldown()
                self._star_rating_to_slam_cooldowns[current_whole_star_rating] = new_cooldown

    def start_off_lot_simulation_time(self):
        self._last_off_lot_update = services.time_service().sim_now

    def should_show_no_way_to_make_money_notification(self):
        return False

    def meets_minimum_employee_requirment(self):
        return True

    def meets_zone_requirement(self):
        zone_director = services.venue_service().get_zone_director()
        return zone_director is not None and zone_director.supports_business_type(self._business_type)

    def meets_requirements_to_be_open(self):
        return self.meets_zone_requirement() and (self.meets_minimum_employee_requirment() and not self.should_show_no_way_to_make_money_notification())

    def _show_appropriate_open_business_notification(self):
        if self.is_owned_by_npc or not self.is_owner_household_active:
            return
        active_sim_info = services.active_sim_info()
        if self.should_show_no_way_to_make_money_notification():
            notification = self.tuning_data.no_way_to_make_money_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
        else:
            notification = self.tuning_data.open_business_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
        notification.show_dialog()

    def _open_business(self, is_npc_business=False):
        self._clear_state()
        self._is_open = True
        self._open_time = services.time_service().sim_now
        self._employee_manager.open_business()
        self._show_appropriate_open_business_notification()
        bucks_tracker = self.get_bucks_tracker()
        if bucks_tracker is not None:
            bucks_tracker.activate_stored_temporary_perk_timers_of_type(self.tuning_data.bucks)
        self._distribute_business_open_status(is_open=True, open_time=self._open_time.absolute_ticks())
        if self.business_zone_id == services.current_zone_id():
            self.tuning_data.lighting_helper_open.execute_lighting_helper(self)
            zone_director = services.venue_service().get_zone_director()
            zone_director.set_customers_allowed(True)
            zone_director.refresh_open_street_director_status()
        else:
            self.start_off_lot_simulation_time()
        sound = PlaySound(services.get_active_sim(), self.tuning_data.audio_sting_open.instance)
        sound.start()
        owner_sim_info = services.sim_info_manager().get(self.owner_sim_id)
        resolver = SingleSimResolver(owner_sim_info)
        for loot in self.tuning_data.loot_list_on_open:
            if loot is SmallBusinessTunables.BUSINESS_EVENTS_LOOT and not self.is_small_business_events_enabled():
                pass
            else:
                loot.apply_to_resolver(resolver)
        self.send_daily_profit_and_cost_update()
        self._send_daily_items_sold_update()
        self._send_review_update_message()
        self._send_business_opened_telemetry()

    def close_business(self, play_sound=True, show_summary_dialog=True) -> 'None':
        self._close_business(play_sound=play_sound, show_summary_dialog=show_summary_dialog)

    def _close_business(self, play_sound=True, show_summary_dialog=True):
        if not self._is_open:
            return
        if play_sound:
            sound = PlaySound(services.get_active_sim(), self.tuning_data.audio_sting_close.instance)
            sound.start()
        self._employee_manager.close_business()
        self.send_daily_profit_and_cost_update()
        self._send_business_closed_telemetry()
        bucks_tracker = self.get_bucks_tracker()
        if bucks_tracker is not None:
            bucks_tracker.deactivate_all_temporary_perk_timers_of_type(self.tuning_data.bucks)
            self.modify_funds(-self._employee_manager.final_daily_wages(), from_item_sold=False, employee_wages=True)
            self.modify_funds(self._employee_manager.household_employee_wages(), from_item_sold=False, employee_wages=True)
        self.on_store_closed()
        services.get_event_manager().process_event(TestEvent.BusinessClosed)
        self._distribute_business_open_status(False)
        business_in_zone = True if self.business_zone_id == services.current_zone_id() else False
        if business_in_zone:
            self.tuning_data.lighting_helper_close.execute_lighting_helper(self)
        else:
            self.run_off_lot_simulation()
            self._last_off_lot_update = None
        self._total_open_hours += int((services.game_clock_service().now() - self._open_time).in_hours())
        if show_summary_dialog and self.is_owner_household_active:
            self.show_summary_dialog(is_from_close=True)
        self._open_time = None
        self._is_open = False
        if business_in_zone:
            zone_director = services.venue_service().get_zone_director()
            if zone_director is not None:
                zone_director.refresh_open_street_director_status()
        if self.tuning_data.loot_list_on_close:
            owner_sim_info = services.sim_info_manager().get(self.owner_sim_id)
            if owner_sim_info is not None:
                resolver = SingleSimResolver(owner_sim_info)
                for loot in self.tuning_data.loot_list_on_close:
                    loot.apply_to_resolver(resolver)

    def _clear_state(self):
        self._open_time = None
        self._daily_revenue = 0
        self._daily_items_sold = 0
        self._funds_category_tracker.clear()
        self._buff_bucket_totals.clear()
        self._buff_bucket_size = 0
        self._employee_manager._clear_state()
        self._customer_manager._clear_state()
        self._off_lot_negative_profit_notification_shown = False

    def get_timespan_since_open(self, is_from_close=False):
        if (self.is_open or is_from_close) and self._open_time is None:
            return
        return services.game_clock_service().now() - self._open_time

    def _send_daily_items_sold_update(self):
        if self.is_active_household_and_zone():
            items_sold_msg = Business_pb2.BusinessDailyItemsSoldUpdate()
            items_sold_msg.zone_id = self.business_zone_id
            items_sold_msg.daily_items_sold = self.daily_items_sold
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_DAILY_ITEMS_SOLD_UPDATE, items_sold_msg)
            Distributor.instance().add_op_with_no_owner(op)

    def is_active_household_and_zone(self):
        return self.is_owner_household_active and (self._zone_id is not None and self._zone_id == services.current_zone_id())

    def is_small_business_events_enabled(self):
        account_data_msg = services.get_persistence_service().get_account_proto_buff()
        options_proto = account_data_msg.gameplay_account_data.gameplay_options
        if options_proto is None:
            return True
        if not hasattr(options_proto, 'small_business_events_enabled'):
            return True
        return options_proto.small_business_events_enabled

    def _get_tuning_data_for_employee_type(self, employee_type):
        return self.tuning_data.employee_data_map.get(employee_type, None)

    def _send_business_opened_telemetry(self):
        with telemetry_helper.begin_hook(business_telemetry_writer, TELEMETRY_HOOK_BUSINESS_OPEN, household=services.household_manager().get(self.owner_household_id)) as hook:
            if self.owner_sim_id is not None:
                hook.write_int(TELEMETRY_HOOK_BUSINESS_ID, self.owner_sim_id)
            hook.write_enum(TELEMETRY_HOOK_BUSINESS_TYPE, self.business_type)

    def _send_business_closed_telemetry(self):
        with telemetry_helper.begin_hook(business_telemetry_writer, TELEMETRY_HOOK_BUSINESS_CLOSED, household=services.household_manager().get(self.owner_household_id)) as hook:
            hook.write_int(TELEMETRY_HOOK_LENGTH_BUSINESS_OPENED, self.minutes_open)
            hook.write_int(TELEMETRY_HOOK_NUM_WORKERS, self.employee_count)
            hook.write_int(TELEMETRY_HOOK_AMOUNT_PROFIT, self.get_daily_net_profit())
            hook.write_float(TELEMETRY_HOOK_STAR_RATING, self.get_star_rating())
            hook.write_enum(TELEMETRY_HOOK_BUSINESS_TYPE, self.business_type)
            if self.owner_sim_id is not None:
                hook.write_int(TELEMETRY_HOOK_BUSINESS_ID, self.owner_sim_id)

    def send_business_funds_update(self):
        if self.is_owner_household_active:
            funds_msg = Business_pb2.BusinessFundsUpdate()
            funds_msg.available_funds = self.funds.money
            if self.business_zone_id is not None:
                funds_msg.zone_id = self.business_zone_id
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_FUNDS_UPDATE, funds_msg)
            Distributor.instance().add_op_with_no_owner(op)

    def send_daily_profit_and_cost_update(self):
        if self.is_owner_household_active:
            profit_msg = Business_pb2.BusinessProfitUpdate()
            profit_msg.zone_id = services.current_zone_id() if self.business_zone_id == None else self.business_zone_id
            profit_msg.net_profit = self.get_daily_net_profit(include_employee_wages=False)
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_PROFIT_UPDATE, profit_msg)
            Distributor.instance().add_op_with_no_owner(op)
            cost_msg = Business_pb2.BusinessDailyCostsUpdate()
            cost_msg.zone_id = services.current_zone_id() if self.business_zone_id == None else self.business_zone_id
            cost_msg.daily_outgoing_costs = int(self.get_daily_outgoing_costs())
            op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_DAILY_OUTGOING_COSTS_UPDATE, cost_msg)
            Distributor.instance().add_op_with_no_owner(op)

    def send_markup_multiplier_message(self):
        if not self.is_active_household_and_zone():
            return
        markup_msg = UI_pb2.RetailMarkupMultiplierMessage()
        for markup_multiplier in self.tuning_data.markup_multiplier_data:
            with ProtocolBufferRollback(markup_msg.markup_multipliers) as multiplier_entry:
                multiplier_entry.name = markup_multiplier.name
                multiplier_entry.multiplier = markup_multiplier.markup_multiplier
                multiplier_entry.is_selected = markup_multiplier.markup_multiplier == self._markup_multiplier
        op = distributor.shared_messages.create_message_op(markup_msg, Consts_pb2.MSG_RETAIL_MARKUP_MULTIPLIER)
        Distributor.instance().add_op_with_no_owner(op)

    def _distribute_business_open_status(self, is_open=True, open_time=0):
        open_msg = Business_pb2.BusinessIsOpenUpdate()
        open_msg.is_open = is_open
        open_msg.time_opened = open_time
        if self.owner_sim_id is not None:
            open_msg.sim_id = self.owner_sim_id
        if self.business_zone_id is not None:
            open_msg.zone_id = self.business_zone_id
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.BUSINESS_OPEN_UPDATE, open_msg)
        Distributor.instance().add_op_with_no_owner(op)

    def _send_review_update_message(self):
        review_msg = Business_pb2.ReviewDataUpdate()
        self._populate_review_update_message(review_msg)
        op = GenericProtocolBufferOp(DistributorOps_pb2.Operation.REVIEW_DATA_UPDATE, review_msg)
        Distributor.instance().add_op_with_no_owner(op)

    def _populate_review_update_message(self, review_msg):
        review_msg.zone_id = self.business_zone_id
        review_msg.score = self.get_star_rating()
        review_msg.review_count = self._customer_manager.lifetime_customers_served
        bottom_growth_msg = review_msg.icons.add()
        top_growth_msg = review_msg.icons.add()
        excellence_msg = review_msg.icons.add()
        (top_performer_bucket, top_performer_value) = self.get_top_performing_bucket_tuple()
        if top_performer_bucket is not None:
            top_bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[top_performer_bucket]
            self._fill_review_data(True, top_bucket_data, excellence_msg, top_bucket_data.bucket_value_maximum - top_performer_value)
        sorted_buckets = self.get_sorted_delta_buff_buckets_tuple()
        if sorted_buckets is None or len(sorted_buckets) == 0:
            return
        top_growth_filled = False
        for (buff_bucket, delta) in sorted_buckets:
            buff_bucket_data = self.tuning_data.customer_star_rating_buff_bucket_data[buff_bucket]
            if not top_growth_filled:
                self._fill_review_data(False, buff_bucket_data, top_growth_msg, delta)
                top_growth_filled = True
            else:
                self._fill_review_data(False, buff_bucket_data, bottom_growth_msg, delta)
                break

    def _fill_review_data(self, is_positive, buff_bucket_data, icon_msg, delta):
        if buff_bucket_data.bucket_hidden_review:
            return
        icon_msg.desc = buff_bucket_data.bucket_excellence_text() if is_positive else buff_bucket_data.bucket_growth_opportunity_text()
        icon_msg.icon = ResourceKey_pb2.ResourceKey()
        icon_msg.icon.instance = buff_bucket_data.bucket_icon.instance
        icon_msg.icon.group = buff_bucket_data.bucket_icon.group
        icon_msg.icon.type = buff_bucket_data.bucket_icon.type
        icon_msg.title = buff_bucket_data.bucket_title

    def populate_employee_msg(self, sim_info, employee_msg, business_employee_type, business_employee_data):
        employee_msg.sim_id = sim_info.sim_id
        employee_data = self._employee_manager.get_employee_data(sim_info)
        employee_is_training = sim_info.has_buff_with_tag(self.tuning_data.employee_training_buff_tag)
        for (skill_type, skill_type_data) in business_employee_data.employee_skills.items():
            with ProtocolBufferRollback(employee_msg.skill_data) as employee_skill_msg:
                employee_skill_msg.skill_id = skill_type.guid64
                employee_skill_msg.curr_points = int(sim_info.get_stat_value(skill_type))
                employee_skill_msg.is_training = employee_is_training
                employee_skill_msg.has_skilled_up = employee_data.has_leveled_up_skill(skill_type) if employee_data is not None else False
                employee_skill_msg.skill_tooltip = skill_type_data.business_summary_description
        if self.is_employee(sim_info):
            satisfaction_stat = sim_info.get_statistic(business_employee_data.satisfaction_commodity)
            statisfaction_state_index = satisfaction_stat.get_state_index()
            if statisfaction_state_index is not None:
                employee_msg.satisfaction_string = satisfaction_stat.states[statisfaction_state_index].buff.buff_type.buff_name(sim_info)
            career_level = self.get_employee_career_level(sim_info)
            employee_msg.pay = career_level.simoleons_per_hour
            career = self.get_employee_career(sim_info)
            employee_msg.current_career_level = career.level
            employee_msg.max_career_level = len(career.current_track_tuning.career_levels) - 1
        else:
            desired_level = self.get_desired_career_level(sim_info, business_employee_type)
            career_level = business_employee_data.career.start_track.career_levels[desired_level]
            employee_msg.pay = career_level.simoleons_per_hour
            employee_msg.current_career_level = desired_level
            employee_msg.max_career_level = len(business_employee_data.career.start_track.career_levels) - 1

    def on_protocols_loaded(self):
        pass

    def on_build_buy_enter(self):
        self.send_business_funds_update()

    def on_build_buy_exit(self):
        self.on_build_buy_exit_money_update()
        if not self.meets_requirements_to_be_open():
            self.set_open(False)

    def on_build_buy_exit_money_update(self):
        if self._owner_household_id is not None:
            owner_household = services.household_manager().get(self._owner_household_id)
            owner_household.funds.send_money_update(vfx_amount=0)

    def on_all_households_and_sim_infos_loaded(self) -> 'None':
        pass

    def on_zone_load(self):
        services.get_event_manager().register(self, self.EVENTS)
        self._employee_manager.on_zone_load()
        self._customer_manager.on_zone_load()
        if self.should_close_after_load():
            self._close_business(play_sound=False)
        self._funds._send_money_update_internal(self._owner_household_id, 0)
        self.send_daily_profit_and_cost_update()
        self._send_daily_items_sold_update()

    def on_client_disconnect(self):
        self._employee_manager.on_client_disconnect()
        event_manager = services.get_event_manager()
        if event_manager is not None:
            event_manager.unregister(self, self.EVENTS)

    def on_registered_perk_callback(self):
        if self.tuning_data.quality_unlock_perk is None:
            return
        if self.is_owner_household_active:
            bucks_tracker = self.get_bucks_tracker()
            if bucks_tracker is not None:
                quality_unlocked = bucks_tracker.is_perk_unlocked(self.tuning_data.quality_unlock_perk)
                if self._quality_unlocked != quality_unlocked:
                    self._quality_unlocked = quality_unlocked
                    self.send_data_to_client()
                if not self._quality_unlocked:
                    bucks_tracker.add_perk_unlocked_callback(self.tuning_data.bucks, self._business_perk_unlocked_callback)
                    current_zone = services.current_zone()
                    if current_zone is not None:
                        current_zone.register_callback(ZoneState.SHUTDOWN_STARTED, self._unregister_perk_unlock_callback)

    def _unregister_perk_unlock_callback(self):
        if not self._quality_unlocked:
            bucks_tracker = self.get_bucks_tracker()
            if bucks_tracker is None:
                return
            bucks_tracker.remove_perk_unlocked_callback(self.tuning_data.bucks, self._business_perk_unlocked_callback)

    def _business_perk_unlocked_callback(self, perk):
        if perk == self.tuning_data.quality_unlock_perk:
            self._unregister_perk_unlock_callback()
            self._quality_unlocked = True
            self._distribute_business_manager_data_message()
            current_zone = services.current_zone()
            if current_zone is not None:
                current_zone.unregister_callback(ZoneState.SHUTDOWN_STARTED, self._unregister_perk_unlock_callback)

    def _distribute_business_manager_data_message(self):
        pass

    def handle_event(self, sim_info, event, resolver):
        if event == TestEvent.SimDeathTypeSet and self.is_employee(sim_info):
            self.remove_employee(sim_info)

    def save_data(self, business_save_data):
        business_save_data.is_open = self._is_open
        business_save_data.funds = self._funds.money
        business_save_data.markup = self._markup_multiplier
        business_save_data.grand_opening = self._grand_opening
        business_save_data.daily_revenue = int(self._daily_revenue)
        business_save_data.daily_items_sold = self._daily_items_sold
        business_save_data.star_rating_value = self._star_rating_value
        self.start_off_lot_simulation_time()
        business_save_data.last_off_lot_update = self._last_off_lot_update
        if self._open_time is not None:
            business_save_data.open_time = self._open_time.absolute_ticks()
        business_save_data.total_open_hours = self._total_open_hours
        for (funds_category, amount) in self._funds_category_tracker.items():
            with ProtocolBufferRollback(business_save_data.funds_category_tracker_data) as funds_category_msg:
                funds_category_msg.funds_category = funds_category
                funds_category_msg.amount = int(amount)
        for (buff_bucket, buff_bucket_total) in self._buff_bucket_totals.items():
            with ProtocolBufferRollback(business_save_data.buff_bucket_totals) as buff_bucket_total_msg:
                buff_bucket_total_msg.buff_bucket = buff_bucket
                buff_bucket_total_msg.buff_bucket_total = buff_bucket_total
        business_save_data.buff_bucket_size = self._buff_bucket_size
        self._employee_manager.save_data(business_save_data.employee_payroll)
        self._customer_manager.save_data(business_save_data)
        zone_data = services.get_persistence_service().get_zone_proto_buff(self.business_zone_id)
        if zone_data is not None:
            venue_business_save_data = self.create_venue_business_data_proto()
            if venue_business_save_data is not None:
                venue_data = zone_data.gameplay_zone_data.venue_data
                venue_data.business_data = venue_business_save_data

    def load_data(self, business_save_data, is_legacy=False):
        self._is_open = business_save_data.is_open
        self._markup_multiplier = business_save_data.markup
        self._grand_opening = business_save_data.grand_opening
        self._daily_revenue = business_save_data.daily_revenue
        self._daily_items_sold = business_save_data.daily_items_sold
        if self._is_open:
            self._open_time = DateAndTime(business_save_data.open_time)
            self._distribute_business_open_status(is_open=self._is_open, open_time=self._open_time.absolute_ticks())
        else:
            self._distribute_business_open_status(self._is_open)
        for funds_category_msg in business_save_data.funds_category_tracker_data:
            self._funds_category_tracker[funds_category_msg.funds_category] = funds_category_msg.amount
        self._total_open_hours = business_save_data.total_open_hours
        self._funds = BusinessFunds(self._owner_household_id, business_save_data.funds, self)
        if is_legacy:
            self._employee_manager.load_legacy_data(business_save_data)
        else:
            self._buff_bucket_totals.clear()
            for buff_bucket_total_msg in business_save_data.buff_bucket_totals:
                self._buff_bucket_totals[buff_bucket_total_msg.buff_bucket] = buff_bucket_total_msg.buff_bucket_total
            self._last_off_lot_update = DateAndTime(business_save_data.last_off_lot_update)
            self._star_rating_value = business_save_data.star_rating_value
            self._buff_bucket_size = business_save_data.buff_bucket_size
            self._customer_manager.load_data(business_save_data)
            self._employee_manager.load_data(business_save_data.employee_payroll)
        zone_data = services.get_persistence_service().get_zone_proto_buff(self.business_zone_id)
        if zone_data is not None and zone_data.HasField('gameplay_zone_data') and zone_data.gameplay_zone_data.HasField('venue_data'):
            venue_data = zone_data.gameplay_zone_data.venue_data
            if venue_data.HasField('business_data'):
                venue_business_data_proto = venue_data.business_data
                if venue_business_data_proto is not None:
                    self.load_venue_business_data_proto(venue_business_data_proto)

    def create_venue_business_data_proto(self) -> 'Optional[GameplaySaveData_pb2.VenueBusinessData]':
        pass

    def load_venue_business_data_proto(self, venue_business_data_proto:'GameplaySaveData_pb2.VenueBusinessData') -> 'None':
        pass

    def send_venue_business_data_update_message(self) -> 'None':
        business_data_proto = self.create_venue_business_data_proto()
        if business_data_proto is not None:
            services.venue_service().send_business_data_update_message(business_data_proto, self.business_zone_id)

    def on_loading_screen_animation_finished(self):
        self._customer_manager.on_loading_screen_animation_finished()
        if self._grand_opening:
            self._grand_opening = False
            FundsTransferDialog.show_dialog(first_time_buyer=True)
            if self.tuning_data.grand_opening_notification is not None:
                active_sim_info = services.active_sim_info()
                notification = self.tuning_data.grand_opening_notification(active_sim_info, resolver=SingleSimResolver(active_sim_info))
                notification.show_dialog()

    def get_bucks_tracker(self):
        if self.owner_household_id is None:
            return
        owning_household = services.household_manager().get(self.owner_household_id)
        if owning_household is None:
            return
        return owning_household.bucks_tracker

    def get_interaction_score_multiplier(self, interaction, sim=DEFAULT):
        return 1

    def transfer_business(self, new_owner_id:'int'):
        pass

    def on_zoneless_owner_sim_changed_household(self, new_owner_id:'int', household_id:'int') -> 'None':
        pass

    def on_fire_ended(self):
        pass

    def on_death(self, sim_info:'SimInfo'):
        pass

    def handle_on_lot_not_owned_anymore(self, zone_id):
        pass

    def is_zone_assigned_allowed(self, zone_id):
        return False

    def reset_allowed_zones(self):
        pass

    def get_allowed_zone_ids(self):
        pass

    def add_allowed_zone_id(self, zone_id:'int') -> 'None':
        pass

