import os
from typing import List
import time
import re
import subprocess
from pathlib import Path
from utils.logging import get_logger

from employee import Employee
from data import Instance, Solution, BusLeg
from destroyer import DestroyOutput
from stats import Stats


logger = get_logger(__name__)


class CG():

    def __init__(self, config: dict, instance_name: str, max_branching: int, verbose: bool) -> None:
        self.instance_name = instance_name
        # insert relative path / 'files' / 'instances' / f'{self.instance_name}_dist.csv'
        self.distance_file = Path('.') / 'files' / 'instances' / f'{self.instance_name}_dist.csv'
        # self.distance_file = Path.home() / 'busdriverschedulingproblem' / 'files' / 'instances' / f'{self.instance_name}_dist.csv'
        self.extra_file = Path('.')  / 'files' / 'instances' / f'{self.instance_name}_extra.csv'
        self.version = config['BP_version']
        self.java_name = Path.home() / 'bin' / 'jdk-20' / 'bin' / 'java'
        self.timeout = config['BP_budget']
        self.maxBranching = max_branching
        self.initial_solution = config['BP_initial_solution']
        self.columns_out = config['BP_columns_out']
        self.redirect_output = Path('.')  / 'logs' / f'{instance_name}_output.txt'
        self.verbose = verbose

    def get_arguments(self, instance_file: str) -> str:
        """ Return the arguments for the CG

        Parameters
        ----------
        instance_file : str
            the name of the instance file

        Returns
        -------
        List[str]
            List that containts all the arguments that CG needs
        """

        output = f' -i {instance_file} '
        output += f'-d {self.distance_file} '
        output += f'-e {self.extra_file} '
        output += f'-t {self.timeout} '
        if self.maxBranching is not None:
            output += f'-b {self.maxBranching} '                            # Maximum number of branching steps, 0 for only CG
        output += '-c 1 '
        output += f"--log {Path('.')  / 'logs' / self.instance_name}_cg.log "
        output += f"--cplex-log {Path('.')  / 'logs' / self.instance_name}_cg_cplex.log "
        output += '-v 0'

        return output.rstrip('\n')

    def run(self, arguments: List[str]) -> str:
        """Run the Column Generation method

        Parameters:
        ----------

        arguments: List[str]
            arguments needed for CG

        Returns
        -------
        str
           name of the output_file that containts the solution
        """
        try:
            logger.debug(f'Running CG with {arguments=}')
            args = [self.java_name, '-jar', Path('.') / 'busdriver_cg.jar'] + arguments.split(' ')
            if self.verbose:
                proc = subprocess.run(
                    args=args,
                    stderr=subprocess.PIPE,
                    stdout=subprocess.PIPE
                    )
            else:
                proc = subprocess.run(
                    args=args,
                    stderr=subprocess.PIPE,
                    stdout=subprocess.PIPE
                    )
            output = proc.stdout.decode('utf-8')
        except:
            logger.exception(f'CG did not work! {arguments=}')
            return None

    # Check if the output contains characters other than ";", ",", "\n", and numbers
        pattern = r'[^;,0-9\n]'
        if re.search(pattern, output):
            logger.debug('Output contains invalid characters')
            return None
        return output

class Repairer():

    def __init__(self,
                 instance_name: str,
                 config: dict,
                 stats: Stats,
                 instance: Instance,
                 size: int,
                 file_name: str = None,
                 max_branching='0',
                 verbose: bool = False
                 ) -> None:
        self.instance_name = instance_name
        self.config = config
        self.stats = stats
        self.instance = instance
        self.name = 'CG'
        self.size = size
        self.cg = CG(self.config, self.instance_name, max_branching, verbose)
        self.file_name = file_name

    def create_input_file(self, destroyed_legs: List[BusLeg]) -> str:
        """Create the instance file to be used by the BP

        Parameters
        ----------
        destroy_legs : List[BusLeg]
            legs destroyed by the destroyer

        Returns
        -------
        str
            name of the instance file
        """
        file_name = self.file_name
        destroyed_legs.sort(key=lambda x: x.tour)
        with open(file_name, 'w') as f:
            f.write('tour,start,end,startPos,endPos\n')
            for leg in destroyed_legs:
                f.write(
                    f'{leg.tour},{leg.start},{leg.end},{leg.start_pos},{leg.end_pos}\n'
                    )
        if os.path.exists(file_name):
            logger.debug(f'Created input file {file_name=} with {len(destroyed_legs)=} legs')
        return file_name



    def reconstruct_solution(self,
                             destroyed_legs: List[BusLeg],
                             solution: Solution,
                             source: str) -> Solution:
        """ 
        Reconstruct the solution from the output of the BP

        Parameters
        ----------
        destroyed Legs : List[BusLeg]
            legs that have been destroyed
        solution : Solution
            solution to be reconstructed
        file_name : str
            name of the file with the output of the BP

        Returns
        -------
        Solution
            reconstructed solution
        """
        destroyed_legs.sort(key=lambda x: (x.start, x.tour))
        employees = []
        counter = 10000
        source = source.split('\n')
        empty = True
        for row in source:
            if "#" in row:
                return None
            if not row:
                continue
            empty = False
            employee = Employee(counter, self.instance)
            employee.name = f'E{str(counter)}'
            counter += 1
            row = row.split(';')
            # index of elements in row that are equals 1
            row_legs = [index for index, value in enumerate(row) if value == '1']          
            if not row_legs:
                logger.debug(f'Empty row {row=}')
                continue
            for leg_index in row_legs:
                if leg_index < len(destroyed_legs):
                    employee.add_bus(destroyed_legs[leg_index])
                else:
                    logger.debug(f'Leg {leg_index=} not in destroyed_legs')
            employees.append(employee)
        if empty:
            logger.debug(f'Empty solution {source=}')
            return None
        else:
            new_solution = Solution(employees)
            solution.employees.extend(new_solution)

            return solution
    

    def apply(self,
              solution: Solution,
              destroy_output: DestroyOutput):
        current_solution = solution.copy()
        instance_file = self.create_input_file(destroy_output.legs)
        arguments = self.cg.get_arguments(instance_file)
        start_exact = time.perf_counter()
        try:
            output_cg = self.cg.run(arguments)
            logger.debug(f'CG ran in {time.perf_counter() - start_exact:.2f} seconds')
        except subprocess.CalledProcessError:
            logger.exception('CG did not work!')
            return solution
        except Exception as e:
            logger.exception(f'CG did not work! {arguments=}. Error={e}')
            return solution
        if output_cg is None:
            logger.debug(f'CG did not work! {arguments=}')
            self.stats.update_repairer(self.name, 'ERROR', time.perf_counter() - start_exact)
            return current_solution
        solution = self.reconstruct_solution(destroy_output.legs, solution, output_cg)
        if solution is None:
            solution = current_solution.copy()
            self.stats.update_repairer(self.name, 'ERROR', time.perf_counter() - start_exact)
            return solution
        solution.resort_employees()
        solution.evaluate()
        if [e for e in solution.employees if e.state.feasible is False]:
            self.stats.update_repairer(self.name, 'INFEASIBLE', time.perf_counter() - start_exact)
            logger.warning(f'INFEASIBLE SOLUTION AFTER BP! {solution.value=}')            
            solution = current_solution.copy()
        else:
            self.stats.update_repairer(self.name, 'OPTIMAL', time.perf_counter() - start_exact)
        return solution
