import random
from .commoditys import commoditys as cm
from .exchange import Exchange
from .business import Price_Believe_Business
import uuid
import time
import logging
from concurrent.futures import ThreadPoolExecutor 
import log
class Simulation():
    """
    Class for controlling the different calculation steps of the Simulation.
    """
    def __init__(self) -> None:
        self.timings={}
        self.tick_funcs={}
        self.reset_funcs={}
        self.tick_count=0
        self.episode_count=-1
        self.cells={}
        self.populated_cells={}
        self.businesses=[]
        self.production_util={}
        self.taskpool=ThreadPoolExecutor()

        self.setup_timing(["dss","dsa","dab","dbl","dt"])
        for k in cm.productions.items():
            l=k[1]
            for i in l:
                self.production_util[i["id"]]=0
        self.cx=Exchange()
        pass

    def set_cells(self,cells):
        for c in cells:
            self.cells[c.name]=c
        
    def set_businesses(self,bus):
        self.businesses=bus
    def seed(self,a):
        """
        Sets the random seed
        """
        random.seed(a)
    def register_agent(self,id,tickfunc,resetfunc):
        
        self.tick_funcs[id]=tickfunc
        self.reset_funcs[id]=resetfunc
    
    def unregister_agent(self,id):
        """
        Unregisters agent
        """
        self.tick_funcs.pop(id,None)
        self.reset_funcs.pop(id,None)


    def tick_agents_random_order(self):
        """
        Ticks all agents in a Random Order
        """
        keys=list(self.tick_funcs.keys())
        random.shuffle(keys)
        for k in keys:
            fun=self.tick_funcs[k]
            fun(self.tick_count,self.episode_count)
        

    def tick(self,episode_length):
        """
        Executes a tick of the simulation
        """
        start=time.time()

        for cell_id,c in self.cells.items():
            c.setup_demand_for_step(episode_length)
        setup=time.time()
        self.tick_agents_random_order()
        agentstep=time.time()
        for b in self.businesses:
            b.tick(self.tick_count)
            
        busstep=time.time()
        self.tick_count+=1
        self.log_cxs_tick(episode_length)
        self.log_business_tick(episode_length)
        logstep=time.time()
        self.timings["dss"]+=setup-start
        self.timings["dsa"]+=agentstep-setup
        self.timings["dab"]+=busstep-agentstep
        self.timings["dbl"]+=logstep-busstep
        self.timings["dt"]+=logstep-start
   
        

    def reset(self):
        """
        Resets all agents to new Episode
        """
        self.log_cxs_episode()
        for k,v in self.reset_funcs.items():
            v(self.episode_count)
        for cell_id,cell in self.cells.items():
            cell.setup_supply_for_episode()
        for b in self.businesses:
            if b.balance<=0:
                self.remove_business(b.id)
        self.tick_count=0
        self.episode_count+=1
    
    def get_underutelized_prods(self,threshold):
        """
        Returns all productions that fall under the given threshold
        """
        under=[]
        for k,v in self.production_util.items():
            if v<threshold:
                under.append(k)
        return under

    
    def create_bussiness(self,min_available_per_prod):
        """
        Create a new business based on given econemy state.
        Return Business
        """
        selected_prod=None
        # Create a business for every production rule out there
        under_prods=self.get_underutelized_prods(min_available_per_prod)
        if len(under_prods)>0:
            # we need to create more businesses
            selected_prod=random.choice(under_prods)
        else:
            selected_prod=self.get_max_profit_prod()

        
        prod=cm.productions_id_map[selected_prod]
        # Now select cell to place business
        cells=self.select_business_cell(prod)
        cell_id=random.choice(cells)
        cell=self.cells[cell_id]
        cxs=[cell.exchange,self.cx]
        self.populated_cells[cell_id]=True
        business=Price_Believe_Business.Price_Believe_Business(uuid.uuid4(),prod,1000,cxs,self)
        self.businesses.append(business)
        self.production_util[selected_prod]+=1
        return business

    def remove_business(self,id):
        bus=None
        for busl in self.businesses:
            if busl.id==id:
                bus=busl
                break

        bus.close_business()
        self.businesses.remove(bus)
        self.production_util[bus.production["id"]]-=1


    def get_max_profit_prod(self):
        """
        Returns the prod with the highest profit in last episode
        """
        profit={}

        for b in self.businesses:
            id=b.production["id"]
            diff=b.balance-b.balance_history[-1]
            if id not in profit:
                profit[id]=0
            profit[id]+=diff
        best=None
        bestv=-1000000000000
        for k,v in profit.items():
            if bestv<v:
                bestv=v
                best=k
        return best



    def select_business_cell(self,prod):
        """
        Calculate score for placement. Return cell with best score.
        """
        cell_score={self.cells[k].name: 0 for k in self.cells}

       
        for id,cell in self.cells.items():
            cx=cell.exchange
            # Best prod resource availability
            # Using the exchange in that cell, lookup supply or demand of resources. Supply +1 / Demand -1
            for prod_item in prod["prod"]:
                key=list(prod_item.keys())[0]
                val=prod_item[key]
                a=cx.get_total_supply(key)
                cell_score[cell.name]+=(cx.get_total_supply(key)/val)
                cell_score[cell.name]-=(cx.get_total_demand(key)/val)
            # If demand for prod item in cell then score +1/ score -1 if supply is already there
            a=cx.get_total_demand(prod["name"])
            cell_score[cell.name]+=cx.get_total_demand(prod["name"])
            cell_score[cell.name]-=cx.get_total_supply(prod["name"])
        max_keys = [key for key, value in cell_score.items() if value == max(cell_score.values())]
        return max_keys
    
    def log_cxs_tick(self,epi_length):
        items=[]
        for id,_ in self.populated_cells.items():
            cell=self.cells[id]
            lcx=cell.exchange
            name="lcx_{}".format(id)
            items.append([lcx,name,self.episode_count,epi_length,self.tick_count])
        results=map(self.launch_cxs_log,items)
        for result in results:
            for id,item in result.items():
                log.EXBooksData.append(item)
        self.cx.log_step("cx",self.episode_count,epi_length,self.tick_count)
    
    def launch_cxs_log(self,args):
        """
        Runs in a conncurrent map
        """
        cx=args[0]
        name=args[1]
        epi=args[2]
        epilen=args[3]
        tick=args[4]
        return cx.log_step(name,epi,epilen,tick,False)
    
    def log_cxs_episode(self):
        for id,_ in self.populated_cells.items():
            cell=self.cells[id]
            lcx=cell.exchange
            name="lcx_{}".format(id)
            lcx.log_episode(name,self.episode_count)
        self.cx.log_episode("cx",self.episode_count)

    def log_business_tick(self,episode_length):
        for bus in self.businesses:
            bus.log(self.episode_count,episode_length,self.tick_count)
    
    def setup_timing(self,metrics):
        for met in metrics:
            self.timings[met]=0