"""Main app for backend api"""
import logging
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, BackgroundTasks

from .logging_config import configure_logging
from .structs.landmark import Landmark
from .structs.preferences import Preferences
from .structs.linked_landmarks import LinkedLandmarks
from .structs.trip import Trip
from .landmarks.landmarks_manager import LandmarkManager
from .toilets.toilet_routes import router as toilets_router
from .optimization.optimizer import Optimizer
from .optimization.refiner import Refiner
from .overpass.overpass import fill_cache
from .cache import client as cache_client


logger = logging.getLogger(__name__)

manager = LandmarkManager()
optimizer = Optimizer()
refiner = Refiner(optimizer=optimizer)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Function to run at the start of the app"""
    logger.info("Setting up logging")
    configure_logging()
    yield
    logger.info("Shutting down logging")


app = FastAPI(lifespan=lifespan)



app.include_router(toilets_router)


@app.post("/trip/new")
def new_trip(preferences: Preferences,
             start: tuple[float, float],
             end: tuple[float, float] | None = None,
             background_tasks: BackgroundTasks = None) -> Trip:
    """
    Main function to call the optimizer.

    Args:
        preferences : the preferences specified by the user as the post body
        start       : the coordinates of the starting point
        end         : the coordinates of the finishing point
    Returns: 
        (uuid) : The uuid of the first landmark in the optimized route
    """
    if preferences is None:
        raise HTTPException(status_code=406, detail="Preferences not provided or incomplete.")
    if (preferences.shopping.score == 0 and
        preferences.sightseeing.score == 0 and
        preferences.nature.score == 0) :
        raise HTTPException(status_code=406, detail="All preferences are 0.")
    if start is None:
        raise HTTPException(status_code=406, detail="Start coordinates not provided")
    if not (-90 <= start[0] <= 90 or -180 <= start[1] <= 180):
        raise HTTPException(status_code=422, detail="Start coordinates not in range")
    if end is None:
        end = start
        logger.info("No end coordinates provided. Using start=end.")

    logger.info(f"Requested new trip generation. Details:\n\tCoordinates: {start}\n\tTime: {preferences.max_time_minute}\n\tSightseeing: {preferences.sightseeing.score}\n\tNature: {preferences.nature.score}\n\tShopping: {preferences.shopping.score}")

    start_landmark = Landmark(name='start',
                              type='start',
                              location=(start[0], start[1]),
                              osm_type='start',
                              osm_id=0,
                              attractiveness=0,
                              duration=0,
                              must_do=True,
                              n_tags = 0)

    end_landmark = Landmark(name='finish',
                            type='finish',
                            location=(end[0], end[1]),
                            osm_type='end',
                            osm_id=0,
                            attractiveness=0,
                            duration=0,
                            must_do=True,
                            n_tags=0)

    start_time = time.time()

    # Generate the landmarks from the start location
    landmarks, landmarks_short = manager.generate_landmarks_list(
        center_coordinates = start,
        preferences = preferences
    )

    if len(landmarks) == 0 :
        raise HTTPException(status_code=500, detail="No landmarks were found.")

    # insert start and finish to the landmarks list
    landmarks_short.insert(0, start_landmark)
    landmarks_short.append(end_landmark)

    t_generate_landmarks = time.time() - start_time
    logger.info(f'Fetched {len(landmarks)} landmarks in  \t: {round(t_generate_landmarks,3)} seconds')
    start_time = time.time()

    # First stage optimization
    try:
        base_tour = optimizer.solve_optimization(preferences.max_time_minute, landmarks_short)
    except Exception as exc:
        logger.error(f"Trip generation failed: {str(exc)}")
        raise HTTPException(status_code=500, detail=f"Optimization failed: {str(exc)}") from exc

    t_first_stage = time.time() - start_time
    start_time = time.time()

    # Second stage optimization
    # TODO : only if necessary (not enough landmarks for ex.)
    try :
        refined_tour = refiner.refine_optimization(landmarks, base_tour,
                                               preferences.max_time_minute,
                                               preferences.detour_tolerance_minute)
    except Exception as exc :
        logger.warning(f"Refiner failed. Proceeding with base trip {str(exc)}")
        refined_tour = base_tour

    t_second_stage = time.time() - start_time

    logger.debug(f'First stage optimization\t: {round(t_first_stage,3)} seconds')
    logger.debug(f'Second stage optimization\t: {round(t_second_stage,3)} seconds')
    logger.info(f'Total computation time\t: {round(t_first_stage + t_second_stage,3)} seconds')
    linked_tour = LinkedLandmarks(refined_tour)

    # upon creation of the trip, persistence of both the trip and its landmarks is ensured.
    trip = Trip.from_linked_landmarks(linked_tour, cache_client)
    logger.info(f'Generated a trip of {trip.total_time} minutes with {len(refined_tour)} landmarks in {round(t_generate_landmarks + t_first_stage + t_second_stage,3)} seconds.')
    logger.debug('Detailed trip :\n\t' + '\n\t'.join(f'{landmark}' for landmark in refined_tour))

    background_tasks.add_task(fill_cache)

    return trip


#### For already existing trips/landmarks
@app.get("/trip/{trip_uuid}")
def get_trip(trip_uuid: str) -> Trip:
    """
    Look-up the cache for a trip that has been previously generated using its identifier.

    Args:
        trip_uuid (str) : unique identifier for a trip.

    Returns:
        (Trip)          : the corresponding trip.
    """
    try:
        trip = cache_client.get(f"trip_{trip_uuid}")
        return trip
    except KeyError as exc:
        logger.error(f"Failed to fetch trip with UUID {trip_uuid}: {str(exc)}")
        raise HTTPException(status_code=404, detail="Trip not found") from exc


@app.get("/landmark/{landmark_uuid}")
def get_landmark(landmark_uuid: str) -> Landmark:
    """
    Returns a Landmark from its unique identifier.

    Args:
        landmark_uuid (str) : unique identifier for a Landmark.

    Returns:
        (Landmark)          : the corresponding Landmark.
    """
    try:
        landmark = cache_client.get(f"landmark_{landmark_uuid}")
        return landmark
    except KeyError as exc:
        logger.error(f"Failed to fetch landmark with UUID {landmark_uuid}: {str(exc)}")
        raise HTTPException(status_code=404, detail="Landmark not found") from exc


@app.post("/trip/recompute-time/{trip_uuid}/{removed_landmark_uuid}")
def update_trip_time(trip_uuid: str, removed_landmark_uuid: str) -> Trip:
    """
    Updates the reaching times of a given trip when removing a landmark.

    Args:
        landmark_uuid (str) : unique identifier for a Landmark.

    Returns:
        (Landmark)          : the corresponding Landmark.
    """
    # First, fetch the trip in the cache.
    try:
        trip = cache_client.get(f'trip_{trip_uuid}')
    except KeyError as exc:
        logger.error(f"Failed to update trip with UUID {trip_uuid} (trip not found): {str(exc)}")
        raise HTTPException(status_code=404, detail='Trip not found') from exc

    landmarks = []
    next_uuid = trip.first_landmark_uuid

    # Extract landmarks
    try :
        while next_uuid is not None:
            landmark = cache_client.get(f'landmark_{next_uuid}')
            # Filter out the removed landmark.
            if next_uuid != removed_landmark_uuid :
                landmarks.append(landmark)
            next_uuid = landmark.next_uuid  # Prepare for the next iteration
    except KeyError as exc:
        logger.error(f"Failed to update trip with UUID {trip_uuid} : {str(exc)}")
        raise HTTPException(status_code=404, detail=f'landmark {next_uuid} not found') from exc

    # Re-link every thing and compute times again
    linked_tour = LinkedLandmarks(landmarks)
    trip = Trip.from_linked_landmarks(linked_tour, cache_client)

    return trip
