from fastapi import HTTPException, status
from sqlalchemy.orm import Session
import numpy as np
import datetime

import activities.activity_streams.constants as activity_streams_constants
import activities.activity_streams.schema as activity_streams_schema
import activities.activity_streams.models as activity_streams_models

import activities.activity.crud as activity_crud
import activities.activity.models as activity_models
import activities.activity.schema as activities_schema

import server_settings.utils as server_settings_utils

import users.user.crud as users_crud

import core.logger as core_logger


def get_activity_streams(
    activity_id: int, token_user_id: int, db: Session
) -> list[activity_streams_schema.ActivityStreams] | None:
    try:
        activity = activity_crud.get_activity_by_id(activity_id, db)

        if not activity:
            # If the activity does not exist, return None
            return None

        # Get the activity streams from the database
        activity_streams = (
            db.query(activity_streams_models.ActivityStreams)
            .filter(
                activity_streams_models.ActivityStreams.activity_id == activity_id,
            )
            .all()
        )

        # Check if there are activity streams if not return None
        if not activity_streams:
            return None

        user_is_owner = True
        if token_user_id != activity.user_id:
            user_is_owner = False

        if not user_is_owner:
            activity_streams = [
                stream
                for stream in activity_streams
                if not (
                    (
                        activity.hide_hr
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_HR
                    )
                    or (
                        activity.hide_power
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_POWER
                    )
                    or (
                        activity.hide_cadence
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_CADENCE
                    )
                    or (
                        activity.hide_elevation
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_ELEVATION
                    )
                    or (
                        activity.hide_speed
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_SPEED
                    )
                    or (
                        activity.hide_pace
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_PACE
                    )
                    or (
                        activity.hide_map
                        and stream.stream_type
                        == activity_streams_constants.STREAM_TYPE_MAP
                    )
                )
            ]

        # Return the activity streams
        return [
            transform_activity_streams(stream, activity, db)
            for stream in activity_streams
        ]
    except Exception as err:
        # Log the exception
        core_logger.print_to_log(
            f"Error in get_activity_streams: {err}", "error", exc=err
        )
        # Raise an HTTPException with a 500 Internal Server Error status code
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal Server Error",
        ) from err


def get_activities_streams(
    activity_ids: list[int],
    token_user_id: int,
    db: Session,
    activities: list[activities_schema.Activity] = None,
) -> list[activity_streams_schema.ActivityStreams]:
    try:
        if not activity_ids:
            return []

        if not activities:
            activities = (
                db.query(activity_models.Activity)
                .filter(activity_models.Activity.id.in_(activity_ids))
                .all()
            )

        if not activities:
            return []

        # Map: activity_id -> activity
        activity_map = {activity.id: activity for activity in activities}

        # Filter out hidden sets for activities the user doesn't own
        allowed_ids = [
            activity.id for activity in activities if activity.user_id == token_user_id
        ]

        if not allowed_ids:
            return []

        # Fetch all streams for the given activity IDs
        all_streams = (
            db.query(activity_streams_models.ActivityStreams)
            .filter(
                activity_streams_models.ActivityStreams.activity_id.in_(allowed_ids)
            )
            .all()
        )

        if not all_streams:
            return []

        # Transform all allowed streams
        return [
            transform_activity_streams(stream, activity_map[stream.activity_id], db)
            for stream in all_streams
        ]

    except Exception as err:
        core_logger.print_to_log(
            f"Error in get_activities_streams: {err}", "error", exc=err
        )
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal Server Error",
        ) from err


def get_public_activity_streams(activity_id: int, db: Session):
    try:
        # Check if public sharable links are enabled in server settings
        server_settings = server_settings_utils.get_server_settings(db)

        # Return None if public sharable links are disabled
        if not server_settings.public_shareable_links:
            return None

        activity = activity_crud.get_activity_by_id_if_is_public(activity_id, db)

        if not activity:
            # If the activity does not exist, return None
            return None

        # Get the activity streams from the database
        activity_streams = (
            db.query(activity_streams_models.ActivityStreams)
            .join(
                activity_models.Activity,
                activity_models.Activity.id
                == activity_streams_models.ActivityStreams.activity_id,
            )
            .filter(
                activity_streams_models.ActivityStreams.activity_id == activity_id,
                activity_models.Activity.visibility == 0,
                activity_models.Activity.id == activity_id,
            )
            .all()
        )

        # Check if there are activity streams, if not return None
        if not activity_streams:
            return None

        activity_streams = [
            stream
            for stream in activity_streams
            if not (
                (
                    activity.hide_hr
                    and stream.stream_type == activity_streams_constants.STREAM_TYPE_HR
                )
                or (
                    activity.hide_power
                    and stream.stream_type
                    == activity_streams_constants.STREAM_TYPE_POWER
                )
                or (
                    activity.hide_cadence
                    and stream.stream_type
                    == activity_streams_constants.STREAM_TYPE_CADENCE
                )
                or (
                    activity.hide_elevation
                    and stream.stream_type
                    == activity_streams_constants.STREAM_TYPE_ELEVATION
                )
                or (
                    activity.hide_speed
                    and stream.stream_type
                    == activity_streams_constants.STREAM_TYPE_SPEED
                )
                or (
                    activity.hide_pace
                    and stream.stream_type
                    == activity_streams_constants.STREAM_TYPE_PACE
                )
                or (
                    activity.hide_map
                    and stream.stream_type == activity_streams_constants.STREAM_TYPE_MAP
                )
            )
        ]

        # Return the activity streams
        return [
            transform_activity_streams(stream, activity, db)
            for stream in activity_streams
        ]
    except Exception as err:
        # Log the exception
        core_logger.print_to_log(
            f"Error in get_public_activity_streams: {err}", "error", exc=err
        )
        # Raise an HTTPException with a 500 Internal Server Error status code
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal Server Error",
        ) from err


def get_activity_stream_by_type(
    activity_id: int, stream_type: int, token_user_id: int, db: Session
):
    try:
        activity = activity_crud.get_activity_by_id(activity_id, db)

        if not activity:
            # If the activity does not exist, return None
            return None

        # Get the activity stream from the database
        activity_stream = (
            db.query(activity_streams_models.ActivityStreams)
            .filter(
                activity_streams_models.ActivityStreams.activity_id == activity_id,
                activity_streams_models.ActivityStreams.stream_type == stream_type,
            )
            .first()
        )

        # Check if there are activity stream if not return None
        if not activity_stream:
            return None

        user_is_owner = True
        if token_user_id != activity.user_id:
            user_is_owner = False

        if not user_is_owner:
            if (
                activity.hide_hr
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_HR
            ):
                return None
            if (
                activity.hide_power
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_POWER
            ):
                return None
            if (
                activity.hide_cadence
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_CADENCE
            ):
                return None
            if (
                activity.hide_elevation
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_ELEVATION
            ):
                return None
            if (
                activity.hide_speed
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_SPEED
            ):
                return None
            if (
                activity.hide_pace
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_PACE
            ):
                return None
            if (
                activity.hide_map
                and activity_stream.stream_type
                == activity_streams_constants.STREAM_TYPE_MAP
            ):
                return None

        # Return the activity stream
        return transform_activity_streams(activity_stream, activity, db)
    except Exception as err:
        # Log the exception
        core_logger.print_to_log(
            f"Error in get_activity_stream_by_type: {err}", "error", exc=err
        )
        # Raise an HTTPException with a 500 Internal Server Error status code
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal Server Error",
        ) from err


def transform_activity_streams(activity_stream, activity, db):
    """
    Transforms an activity stream based on its stream type.
    If the stream type of the given activity_stream is heart rate (HR), this function delegates
    the transformation to the `transform_activity_streams_hr` function. Otherwise, it returns
    the activity_stream unchanged.
    Args:
        activity_stream: The activity stream object to be transformed.
        activity: The activity object associated with the stream.
        db: The database session or connection object.
    Returns:
        The transformed activity stream if the stream type is HR, otherwise the original activity_stream.
    """
    if activity_stream.stream_type == activity_streams_constants.STREAM_TYPE_HR:
        return transform_activity_streams_hr(activity_stream, activity, db)

    return activity_stream


def transform_activity_streams_hr(activity_stream, activity, db):
    """
    Transforms an activity stream by calculating the percentage of time spent in each heart rate zone based on user details.
    Args:
        activity_stream: The activity stream object containing waypoints with heart rate data.
        activity: The activity object associated with the stream, used to retrieve the user ID.
        db: The database session or connection used to fetch user details.
    Returns:
        The activity stream object with an added 'hr_zone_percentages' attribute, which contains the percentage of time spent in each heart rate zone and their respective HR boundaries. If waypoints or user details are missing, returns the original activity stream unchanged.
    Notes:
        - Heart rate zones are calculated using the formula: max_heart_rate = 220 - age.
        - The function expects waypoints to be a list of dicts with an "hr" key.
        - If no valid heart rate data is present, the activity stream is returned as is.
    """
    # Check if the activity stream has waypoints
    waypoints = activity_stream.stream_waypoints
    if not waypoints or not isinstance(waypoints, list):
        # If there are no waypoints, return the activity stream as is
        return activity_stream

    # Get the user details to calculate heart rate zones
    detail_user = users_crud.get_user_by_id(activity.user_id, db)
    if not detail_user or not detail_user.birthdate:
        # If user details are not available or birthdate is missing, return the activity stream as is
        return activity_stream

    # Calculate the maximum heart rate based on the user's birthdate
    year = int(detail_user.birthdate.split("-")[0])
    current_year = datetime.datetime.now().year
    max_heart_rate = 220 - (current_year - year)

    # Calculate heart rate zones based on the maximum heart rate
    zone_1 = max_heart_rate * 0.6
    zone_2 = max_heart_rate * 0.7
    zone_3 = max_heart_rate * 0.8
    zone_4 = max_heart_rate * 0.9

    # Extract heart rate values from waypoints
    hr_values = np.array(
        [float(wp.get("hr")) for wp in waypoints if wp.get("hr") is not None]
    )

    # If there are no valid heart rate values, return the activity stream as is
    total = len(hr_values)
    if total == 0:
        return activity_stream

    # Calculate the percentage of time spent in each heart rate zone
    zone_counts = [
        np.sum(hr_values < zone_1),
        np.sum((hr_values >= zone_1) & (hr_values < zone_2)),
        np.sum((hr_values >= zone_2) & (hr_values < zone_3)),
        np.sum((hr_values >= zone_3) & (hr_values < zone_4)),
        np.sum(hr_values >= zone_4),
    ]
    zone_percentages = [round((count / total) * 100, 2) for count in zone_counts]

    # Calculate zone HR boundaries for display
    zone_hr = {
        "zone_1": f"< {int(zone_1)}",
        "zone_2": f"{int(zone_1)} - {int(zone_2) - 1}",
        "zone_3": f"{int(zone_2)} - {int(zone_3) - 1}",
        "zone_4": f"{int(zone_3)} - {int(zone_4) - 1}",
        "zone_5": f">= {int(zone_4)}",
    }
    activity_stream.hr_zone_percentages = {
        "zone_1": {"percent": zone_percentages[0], "hr": zone_hr["zone_1"]},
        "zone_2": {"percent": zone_percentages[1], "hr": zone_hr["zone_2"]},
        "zone_3": {"percent": zone_percentages[2], "hr": zone_hr["zone_3"]},
        "zone_4": {"percent": zone_percentages[3], "hr": zone_hr["zone_4"]},
        "zone_5": {"percent": zone_percentages[4], "hr": zone_hr["zone_5"]},
    }

    return activity_stream


def get_public_activity_stream_by_type(activity_id: int, stream_type: int, db: Session):
    try:
        # Check if public sharable links are enabled in server settings
        server_settings = server_settings_utils.get_server_settings(db)

        # Return None if public sharable links are disabled
        if not server_settings.public_shareable_links:
            return None

        activity = activity_crud.get_activity_by_id_if_is_public(activity_id, db)

        if not activity:
            # If the activity does not exist, return None
            return None

        # Get the activity stream from the database
        activity_stream = (
            db.query(activity_streams_models.ActivityStreams)
            .join(
                activity_models.Activity,
                activity_models.Activity.id
                == activity_streams_models.ActivityStreams.activity_id,
            )
            .filter(
                activity_streams_models.ActivityStreams.activity_id == activity_id,
                activity_streams_models.ActivityStreams.stream_type == stream_type,
                activity_models.Activity.visibility == 0,
                activity_models.Activity.id == activity_id,
            )
            .first()
        )

        # Check if there is an activity stream; if not, return None
        if not activity_stream:
            return None

        if (
            activity.hide_hr
            and activity_stream.stream_type == activity_streams_constants.STREAM_TYPE_HR
        ):
            return None
        if (
            activity.hide_power
            and activity_stream.stream_type
            == activity_streams_constants.STREAM_TYPE_POWER
        ):
            return None
        if (
            activity.hide_cadence
            and activity_stream.stream_type
            == activity_streams_constants.STREAM_TYPE_CADENCE
        ):
            return None
        if (
            activity.hide_elevation
            and activity_stream.stream_type
            == activity_streams_constants.STREAM_TYPE_ELEVATION
        ):
            return None
        if (
            activity.hide_speed
            and activity_stream.stream_type
            == activity_streams_constants.STREAM_TYPE_SPEED
        ):
            return None
        if (
            activity.hide_pace
            and activity_stream.stream_type
            == activity_streams_constants.STREAM_TYPE_PACE
        ):
            return None
        if (
            activity.hide_map
            and activity_stream.stream_type
            == activity_streams_constants.STREAM_TYPE_MAP
        ):
            return None

        # Return the activity stream
        return transform_activity_streams(activity_stream, activity, db)
    except Exception as err:
        # Log the exception
        core_logger.print_to_log(
            f"Error in get_public_activity_stream_by_type: {err}", "error", exc=err
        )
        # Raise an HTTPException with a 500 Internal Server Error status code
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal Server Error",
        ) from err


def create_activity_streams(
    activity_streams: list[activity_streams_schema.ActivityStreams], db: Session
):
    try:
        # Create a list to store the ActivityStreams objects
        streams = []

        # Iterate over the list of ActivityStreams objects
        for stream in activity_streams:
            # Create an ActivityStreams object
            db_stream = activity_streams_models.ActivityStreams(
                activity_id=stream.activity_id,
                stream_type=stream.stream_type,
                stream_waypoints=stream.stream_waypoints,
                strava_activity_stream_id=stream.strava_activity_stream_id,
            )

            # Append the object to the list
            streams.append(db_stream)

        # Bulk insert the list of ActivityStreams objects
        db.bulk_save_objects(streams)
        db.commit()
    except Exception as err:
        # Rollback the transaction
        db.rollback()

        # Log the exception
        core_logger.print_to_log_and_console(
            f"Error in create_activity_streams: {err}", "error", exc=err
        )
        # Raise an HTTPException with a 500 Internal Server Error status code
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Internal Server Error",
        ) from err
