from datetime import datetime, timedelta
from itertools import permutations
from math import ceil

import requests
from sqlalchemy.orm import Session

from app.infrastructure.database.database import connect
from app.infrastructure.database.models.delivery_accessories import DeliveryAccessory
from app.infrastructure.database.models.delivery_orders import DeliveryOrder
from app.infrastructure.database.models.total_orders import TotalOrder
from app.infrastructure.database.repository.accessory_repository import AccessoriesRepository
from app.infrastructure.database.repository.city_repository import CityRepository
from app.infrastructure.database.repository.delivery_accessory_repository import DeliveryAccessoriesRepository
from app.infrastructure.database.repository.delivery_order_repository import DeliveryOrdersRepository
from app.infrastructure.database.repository.total_order_repository import TotalOrdersRepository
from app.infrastructure.database.repository.truck_repository import TrucksRepository


class NewTotalOrderService:
    def __init__(self, db: Session):
        self.robotsCount = None
        self.total_order_repository = TotalOrdersRepository(db)
        self.delivery_order_repository = DeliveryOrdersRepository(db)
        self.delivery_accessory_repository = DeliveryAccessoriesRepository(db)
        self.city_repository = CityRepository(db)
        self.accessory_repository = AccessoriesRepository(db)
        self.truck_repository = TrucksRepository(db)

    def total_calculate(self, current_user, deadline, robotsCount):
        self.robotsCount = robotsCount

        accessories = self.accessory_repository.get_with_cities_all()
        cities = self.city_repository.get_all_with_federal_district()
        trucks = self.truck_repository.get_all()
        distances = self.create_distance_matrix(cities)

        grouped_by_period = self.group_accessories_by_period(accessories)
        routes_by_period = self.find_routes_by_period(grouped_by_period, distances, cities)

        with connect() as session:
            new_total_order = TotalOrder(
                order_datetime=datetime.now(),
                count_robots=robotsCount,
                deadline=deadline,
                price=0,
                user_id=current_user.id,
                status_id=1,
            )
            session.add(new_total_order)
            session.flush()

            for period, routes in routes_by_period.items():
                for route in routes:
                    vehicle_count, selected_truck = self.calculate_vehicle_requirements(
                        [a for a in accessories if a.period == period], trucks)
                    route_length = self.calculate_route_length(route, distances)

                    selected_truck_capacity = list(
                        filter(
                            lambda truck: truck.id == selected_truck,
                            trucks
                        )
                    )[0].capacity

                    cost = route_length * 8 * selected_truck_capacity / 1000

                    delivery_order_deadline = new_total_order.deadline - timedelta(days=period)

                    new_total_order.price += cost

                    new_delivery_order = DeliveryOrder(
                        order_datetime=datetime.now(),
                        count_trucks=vehicle_count,
                        deadline=delivery_order_deadline,
                        price=cost * vehicle_count,
                        truck_id=selected_truck,
                        total_order_id=new_total_order.id,
                    )

                    session.add(new_delivery_order)
                    session.flush()

                    for point_index in range(len(route)):
                        if route[point_index].name == 'Челябинск':
                            continue

                        accessory = list(
                            filter(
                                lambda x: x.city_id == route[point_index].id and x.period == period,
                                accessories
                            )
                        )[0]

                        new_delivery_accessory = DeliveryAccessory(
                            queue=point_index + 1,
                            accessory_id=accessory.id,
                            delivery_order_id=new_delivery_order.id,
                        )
                        session.add(new_delivery_accessory)

            session.commit()

    def find_routes_by_period(self, grouped_by_period, distances, cities):
        purpose_city_id = self.city_repository.get_by_name('Челябинск').id

        routes_by_period = {}

        for period, accessories in grouped_by_period.items():
            cities_in_period = [
                next(city for city in cities if city.id == accessory.city_id)
                for accessory in accessories
            ]
            all_routes = self.generate_routes(cities_in_period,
                                              next(city for city in cities if city.id == purpose_city_id))

            routes_with_details = []
            for route in all_routes:
                distance = self.calculate_route_distance(route, distances)
                total_weight = sum(
                    sum(accessory.weight for accessory in accessories if accessory.city_id == city.id)
                    for city in route
                )
                cost = self.calculate_route_length(route, distances)
                routes_with_details.append((route, (distance, total_weight, cost)))

            sorted_routes = sorted(routes_with_details, key=lambda x: (x[1][2], x[1][0]))
            routes_by_period[period] = [sorted_routes[0][0]]

        return routes_by_period

    def create_distance_matrix(self, cities):
        num_cities = len(cities)
        distances = [[0] * num_cities for _ in range(num_cities)]

        for i in range(num_cities):
            for j in range(num_cities):
                if i != j:
                    start_coords = (cities[i].x_coordinate, cities[i].y_coordinate)
                    end_coords = (cities[j].x_coordinate, cities[j].y_coordinate)
                    distances[i][j] = self.get_distance(start_coords, end_coords)
                else:
                    distances[i][j] = 0

        return distances

    @staticmethod
    def group_accessories_by_period(accessories):
        grouped = {}
        for accessory in accessories:
            if accessory.period not in grouped:
                grouped[accessory.period] = []
            grouped[accessory.period].append(accessory)
        return grouped

    @staticmethod
    def generate_routes(cities, start_city):
        routes = []
        cities_without_start = [city for city in cities if city.id != start_city.id]
        all_routes = list(permutations(cities_without_start))

        for route in all_routes:
            routes.append([start_city] + list(route) + [start_city])

        return routes

    @staticmethod
    def calculate_route_distance(route, distances):
        total_distance = 0
        for i in range(len(route) - 1):
            total_distance += distances[route[i].id - 1][route[i + 1].id - 1]
        return total_distance

    @staticmethod
    def calculate_route_length(route, distances):
        total_length = 0.0

        for i in range(len(route) - 1):
            total_length += distances[route[i].id - 1][route[i + 1].id - 1]

        return total_length

    def calculate_vehicle_requirements(self, accessories, trucks):
        total_weight = sum(accessory.weight / 100 for accessory in accessories) * self.robotsCount
        total_vehicles = float('inf')
        selected_truck = 0

        for truck in trucks:
            weight_vehicles = ceil(total_weight / truck.capacity)
            if weight_vehicles < total_vehicles:
                total_vehicles = weight_vehicles
                selected_truck = truck.id

        return total_vehicles, selected_truck

    @staticmethod
    def get_distance(start_coords, end_coords):
        base_url = "http://router.project-osrm.org/route/v1/driving"
        url = f"{base_url}/{start_coords[1]},{start_coords[0]};{end_coords[1]},{end_coords[0]}?overview=full"

        response = requests.get(url)
        data = response.json()

        if data['routes']:
            distance = data['routes'][0]['distance'] / 1000
            return distance
        else:
            return float('inf')
