"""Encode a Workout model into a binary FIT file using fit-tool."""

from __future__ import annotations

import time
from pathlib import Path

from fit_tool.fit_file_builder import FitFileBuilder
from fit_tool.profile.messages.file_id_message import FileIdMessage
from fit_tool.profile.messages.workout_message import WorkoutMessage
from fit_tool.profile.messages.workout_step_message import (
    Intensity,
    WorkoutStepDuration,
    WorkoutStepDurationValueField,
    WorkoutStepMessage,
    WorkoutStepTarget,
)
from fit_tool.profile.profile_type import FileType, Manufacturer, Sport

from pace2fit.model import (
    DurationType,
    IntensityType,
    PaceTarget,
    RepeatStep,
    Step,
    Workout,
    WorkoutStep,
    ZoneTarget,
)


# ---------------------------------------------------------------------------
# Intensity mapping
# ---------------------------------------------------------------------------

_INTENSITY_MAP: dict[IntensityType, Intensity] = {
    IntensityType.WARMUP: Intensity.WARMUP,
    IntensityType.ACTIVE: Intensity.ACTIVE,
    IntensityType.COOLDOWN: Intensity.COOLDOWN,
    IntensityType.REST: Intensity.REST,
    IntensityType.RECOVERY: Intensity.RECOVERY,
}


# ---------------------------------------------------------------------------
# Flattening: convert model Steps into a flat list of FIT WorkoutStepMessages
# ---------------------------------------------------------------------------


def _make_step_message(
    index: int,
    step: WorkoutStep,
) -> WorkoutStepMessage:
    """Create a single FIT WorkoutStepMessage from a model WorkoutStep."""
    msg = WorkoutStepMessage()
    msg.message_index = index
    msg.intensity = _INTENSITY_MAP.get(step.intensity, Intensity.ACTIVE)

    # -- Duration --
    # NOTE: fit-tool's duration_time and duration_distance setters have a bug
    # in sub-field resolution that applies the wrong scale factor.  We bypass
    # them entirely and write pre-scaled raw values via set_encoded_value.
    #   TIME:     FIT expects milliseconds  -> seconds * 1000
    #   DISTANCE: FIT expects centimetres   -> metres  * 100
    if step.duration_type == DurationType.TIME:
        msg.duration_type = WorkoutStepDuration.TIME
        if step.duration_value is not None:
            field = msg.get_field(WorkoutStepDurationValueField.ID)
            assert field is not None
            field.set_encoded_value(0, round(step.duration_value * 1000))
    elif step.duration_type == DurationType.DISTANCE:
        msg.duration_type = WorkoutStepDuration.DISTANCE
        if step.duration_value is not None:
            field = msg.get_field(WorkoutStepDurationValueField.ID)
            assert field is not None
            field.set_encoded_value(0, round(step.duration_value * 100))
    else:
        msg.duration_type = WorkoutStepDuration.OPEN

    # -- Target --
    target = step.target
    if target is None:
        msg.target_type = WorkoutStepTarget.OPEN
    elif isinstance(target, ZoneTarget):
        msg.target_type = WorkoutStepTarget.HEART_RATE
        msg.target_hr_zone = target.zone
    elif isinstance(target, PaceTarget):
        msg.target_type = WorkoutStepTarget.SPEED
        # fit-tool's custom_target_speed_* setters don't apply the scale
        # correctly, so we write raw values (m/s * 1000) via the generic
        # custom_target_value_low/high fields.
        msg.custom_target_value_low = round(target.low * 1000)
        msg.custom_target_value_high = round(target.high * 1000)

    if step.name:
        msg.workout_step_name = step.name

    return msg


def _flatten_steps(steps: list[Step]) -> list[WorkoutStepMessage]:
    """Convert a list of model Steps into a flat list of FIT step messages.

    Repeat blocks are expanded into the contained steps followed by a
    ``REPEAT_UNTIL_STEPS_CMPLT`` message.
    """
    messages: list[WorkoutStepMessage] = []

    for step in steps:
        if isinstance(step, WorkoutStep):
            messages.append(_make_step_message(len(messages), step))

        elif isinstance(step, RepeatStep):
            # Record the starting index for the repeat-from pointer
            repeat_start = len(messages)

            # Emit the inner steps
            for inner in step.steps:
                messages.append(_make_step_message(len(messages), inner))

            # Emit the repeat message
            repeat_msg = WorkoutStepMessage()
            repeat_msg.message_index = len(messages)
            repeat_msg.duration_type = WorkoutStepDuration.REPEAT_UNTIL_STEPS_CMPLT
            repeat_msg.duration_step = repeat_start
            repeat_msg.target_repeat_steps = step.count
            messages.append(repeat_msg)

    return messages


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def encode(workout: Workout) -> bytes:
    """Encode a :class:`Workout` into FIT binary data.

    Returns the raw bytes of a valid FIT workout file.
    """
    builder = FitFileBuilder()

    # 1. File ID
    file_id = FileIdMessage()
    file_id.type = FileType.WORKOUT
    file_id.manufacturer = Manufacturer.DEVELOPMENT.value
    file_id.product = 0
    file_id.serial_number = int(time.time())
    builder.add(file_id)

    # 2. Flatten steps
    step_messages = _flatten_steps(workout.steps)

    # 3. Workout header
    workout_msg = WorkoutMessage()
    workout_msg.workout_name = workout.name
    workout_msg.sport = Sport.RUNNING
    workout_msg.num_valid_steps = len(step_messages)
    builder.add(workout_msg)

    # 4. Workout steps
    for msg in step_messages:
        builder.add(msg)

    # 5. Build and return bytes
    fit_file = builder.build()
    return fit_file.to_bytes()


def encode_to_file(workout: Workout, path: str | Path) -> Path:
    """Encode a workout and write it to a ``.fit`` file.

    Args:
        workout: The workout model to encode.
        path: Destination file path.

    Returns:
        The resolved :class:`Path` of the written file.
    """
    out = Path(path)
    out.parent.mkdir(parents=True, exist_ok=True)
    data = encode(workout)
    out.write_bytes(data)
    return out.resolve()
