from __future__ import annotations

from pprint import pprint
from typing import Any, Dict

import yaml

from .building import Building
from .elevator_algorithms.fcfs import FirstComeFirstServedLogic
from .elevator_algorithms.sstf import ShortestSeekTimeFirstLogic
from .elevator_algorithms.scan import ScanLogic
from .elevator_algorithms.look import LookLogic


class Scenario:
    def __init__(self, config_file: str) -> None:
        with open(config_file, 'r', encoding='utf-8') as file:
            self.config: Dict[str, Any] = yaml.safe_load(file)

        self._validate_config()

        self.building: Building = Building(
            n_floors=self.config['n_floors'],
            n_elevators=self.config['n_elevators'],
            elevator_capacity=self.config.get('elevator_capacity', 10)
        )

        if self.config['elevator_logic'] == 'first_come_first_served':
            self.logic = FirstComeFirstServedLogic(self.building)
        elif self.config['elevator_logic'] == 'shortest_seek_time_first':
            self.logic = ShortestSeekTimeFirstLogic(self.building)
        elif self.config['elevator_logic'] == 'scan':
            self.logic = ScanLogic(self.building)
        elif self.config['elevator_logic'] == 'look':
            self.logic = LookLogic(self.building)
        else:
            raise ValueError(f"Unknown elevator logic: {self.config['elevator_logic']}")

        self.current_tick: int = 0
        self.simulation_results: Dict[str, Any] = {
            'scenario_config': self.config,
            'analysis': {
                'total_ticks': 0,
                'total_passengers': 0,
                'ticks': {}
            }
        }

    def run(self, max_ticks: int = 100) -> Dict[str, Any]:
        print(f"Starting simulation with {self.building.to_dict()}")

        total_passengers = sum(
            len(passengers) for passengers in self.config.get('passenger_schedule', {}).values()
        )
        self.simulation_results['analysis']['total_passengers'] = total_passengers

        for tick in range(0, max_ticks):
            self.current_tick = tick
            # -- MAIN SIMULATION LOOP --

            # 1. Handle new passengers appearing at this tick
            self._add_scheduled_passengers(tick)
            self.building.handle_passengers(tick)

            # 2. Assign targets based on onboard and waiting passengers
            self.logic.assign_elevator_targets()

            # 3. Initiate movement
            self.building.tick_movement()

            # Snapshot
            self._record_tick_state(tick)
            # Check if simulation is complete (no more passengers and all elevators idle)
            if self._is_simulation_complete():
                break

        self.simulation_results['analysis']['total_ticks'] = self.current_tick
        print(f"Simulation completed after {self.current_tick} ticks")
        return self.simulation_results

    def _add_scheduled_passengers(self, tick: int) -> None:
        passenger_schedule = self.config.get('passenger_schedule', {})

        if tick in passenger_schedule:
            for passenger_config in passenger_schedule[tick]:
                origin_floor_id = passenger_config['appear_floor']
                destination_floor_id = passenger_config.get('destination_floor')
                passenger_id = passenger_config['passenger_id']

                success = self.building.create_and_add_passenger(
                    passenger_id=passenger_id,
                    origin_floor_id=origin_floor_id,
                    destination_floor_id=destination_floor_id,
                    tick_appeared=tick
                )

                if success:
                    print(f"Tick {tick}: Added passenger {passenger_id} at floor {origin_floor_id} -> {destination_floor_id}")

    def _record_tick_state(self, tick: int) -> None:
        building_state = self.building.to_dict()
        self.simulation_results['analysis']['ticks'][tick] = building_state

    def _is_simulation_complete(self) -> bool:
        max_scheduled_tick = max(self.config.get('passenger_schedule', {}).keys(), default=0)
        if self.current_tick < max_scheduled_tick:
            return False

        # Check if there are any waiting passengers
        for floor in self.building.floors:
            if floor.waiting_passengers:
                return False

        # Check if any elevators have passengers onboard
        for elevator in self.building.elevators:
            if elevator.onboard_passengers:
                return False

        return True

    def _validate_config(self) -> None:
        errors = []

        if 'passenger_schedule' not in self.config:
            raise ValueError("Configuration validation failed:\n  - Missing required field 'passenger_schedule'")

        passenger_schedule = self.config['passenger_schedule']

        if not passenger_schedule:
            raise ValueError("Configuration validation failed:\n  - 'passenger_schedule' cannot be empty")

        seen_passenger_ids = set()

        for tick, passengers in passenger_schedule.items():
            if not isinstance(passengers, list):
                errors.append(f"Tick {tick}: passenger list must be a list, got {type(passengers).__name__}")
                continue

            for idx, passenger_config in enumerate(passengers):
                if not isinstance(passenger_config, dict):
                    errors.append(f"Tick {tick}, passenger {idx}: must be a dict, got {type(passenger_config).__name__}")
                    continue

                # Check required fields
                if 'passenger_id' not in passenger_config:
                    errors.append(f"Tick {tick}, passenger {idx}: missing 'passenger_id' field")
                    continue

                passenger_id = passenger_config['passenger_id']

                # Check passenger ID format (should be a string starting with 'P__')
                if not isinstance(passenger_id, str):
                    errors.append(f"Tick {tick}: passenger_id '{passenger_id}' must be a string, got {type(passenger_id).__name__}")
                elif not passenger_id.startswith('P__'):
                    errors.append(f"Tick {tick}: passenger_id '{passenger_id}' must start with 'P__' (e.g., 'P__0', 'P__1')")

                # Check for duplicate passenger IDs
                if passenger_id in seen_passenger_ids:
                    errors.append(f"Tick {tick}: duplicate passenger_id '{passenger_id}' found")
                else:
                    seen_passenger_ids.add(passenger_id)

                # Check appear_floor format
                if 'appear_floor' not in passenger_config:
                    errors.append(f"Tick {tick}, passenger '{passenger_id}': missing required field 'appear_floor'")
                else:
                    appear_floor = passenger_config['appear_floor']
                    if not isinstance(appear_floor, str):
                        errors.append(f"Tick {tick}, passenger '{passenger_id}': appear_floor must be a string, got {type(appear_floor).__name__}")
                    elif not appear_floor.startswith('F__'):
                        errors.append(f"Tick {tick}, passenger '{passenger_id}': appear_floor '{appear_floor}' must start with 'F__' (e.g., 'F__0', 'F__1')")
                    else:
                        # Check if floor number is valid
                        try:
                            floor_num = int(appear_floor.split('__')[1])
                            if floor_num < 0 or floor_num >= self.config['n_floors']:
                                errors.append(f"Tick {tick}, passenger '{passenger_id}': appear_floor '{appear_floor}' is out of range (0 to {self.config['n_floors']-1})")
                        except (IndexError, ValueError):
                            errors.append(f"Tick {tick}, passenger '{passenger_id}': appear_floor '{appear_floor}' has invalid format")

                # Check destination_floor format
                if 'destination_floor' not in passenger_config:
                    errors.append(f"Tick {tick}, passenger '{passenger_id}': missing required field 'destination_floor'")
                else:
                    destination_floor = passenger_config['destination_floor']
                    if destination_floor is None:
                        errors.append(f"Tick {tick}, passenger '{passenger_id}': destination_floor cannot be None")
                    elif not isinstance(destination_floor, str):
                        errors.append(f"Tick {tick}, passenger '{passenger_id}': destination_floor must be a string, got {type(destination_floor).__name__}")
                    elif not destination_floor.startswith('F__'):
                        errors.append(f"Tick {tick}, passenger '{passenger_id}': destination_floor '{destination_floor}' must start with 'F__' (e.g., 'F__0', 'F__1')")
                    else:
                        # Check if floor number is valid
                        try:
                            floor_num = int(destination_floor.split('__')[1])
                            if floor_num < 0 or floor_num >= self.config['n_floors']:
                                errors.append(f"Tick {tick}, passenger '{passenger_id}': destination_floor '{destination_floor}' is out of range (0 to {self.config['n_floors']-1})")
                        except (IndexError, ValueError):
                            errors.append(f"Tick {tick}, passenger '{passenger_id}': destination_floor '{destination_floor}' has invalid format")

        if errors:
            error_message = "Configuration validation failed:\n" + "\n".join(f"  - {error}" for error in errors)
            raise ValueError(error_message)

    def save(self, output_file: str = None) -> None:
        if output_file is None:
            output_file = 'scenarios_out/simulation_output.yaml'

        with open(output_file, 'w', encoding='utf-8') as file:
            yaml.dump(self.simulation_results, file, default_flow_style=False, indent=2)

        print(f"Simulation results saved to {output_file}")

    def print_summary(self) -> None:
        analysis = self.simulation_results['analysis']
        print("\n=== Simulation Summary ===")
        print(f"Total ticks: {analysis['total_ticks']}")
        print(f"Total passengers: {analysis['total_passengers']}")
        print(f"Building configuration: {self.config['n_floors']} floors, {self.config['n_elevators']} elevators")
        print(f"Elevator capacity: {self.config.get('elevator_capacity', 10)} passengers per elevator")
        print(f"Elevator logic: {self.config['elevator_logic']}")

        print("\nSimulation results:")
        pprint(self.simulation_results, sort_dicts=False)
