"""Service calls for the Teslemetry integration."""

import logging

import voluptuous as vol
from voluptuous import All, Range

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr

from .const import DOMAIN
from .helpers import handle_command, handle_vehicle_command
from .models import TeslemetryEnergyData, TeslemetryVehicleData

_LOGGER = logging.getLogger(__name__)

# Attributes
ATTR_ID = "id"
ATTR_GPS = "gps"
ATTR_TYPE = "type"
ATTR_VALUE = "value"
ATTR_LOCATION = "location"
ATTR_LOCALE = "locale"
ATTR_ORDER = "order"
ATTR_TIMESTAMP = "timestamp"
ATTR_FIELDS = "fields"
ATTR_ENABLE = "enable"
ATTR_TIME = "time"
ATTR_PIN = "pin"
ATTR_TOU_SETTINGS = "tou_settings"
ATTR_PRECONDITIONING_ENABLED = "preconditioning_enabled"
ATTR_PRECONDITIONING_WEEKDAYS = "preconditioning_weekdays_only"
ATTR_DEPARTURE_TIME = "departure_time"
ATTR_OFF_PEAK_CHARGING_ENABLED = "off_peak_charging_enabled"
ATTR_OFF_PEAK_CHARGING_WEEKDAYS = "off_peak_charging_weekdays_only"
ATTR_END_OFF_PEAK_TIME = "end_off_peak_time"
ATTR_DAYS_OF_WEEK = "days_of_week"
ATTR_START_TIME = "start_time"
ATTR_END_TIME = "end_time"
ATTR_ONE_TIME = "one_time"
ATTR_NAME = "name"
ATTR_PRECONDITION_TIME = "precondition_time"

# Services
SERVICE_NAVIGATE_ATTR_GPS_REQUEST = "navigation_gps_request"
SERVICE_SET_SCHEDULED_CHARGING = "set_scheduled_charging"
SERVICE_SET_SCHEDULED_DEPARTURE = "set_scheduled_departure"
SERVICE_VALET_MODE = "valet_mode"
SERVICE_SPEED_LIMIT = "speed_limit"
SERVICE_TIME_OF_USE = "time_of_use"
SERVICE_ADD_CHARGE_SCHEDULE = "add_charge_schedule"
SERVICE_REMOVE_CHARGE_SCHEDULE = "remove_charge_schedule"
SERVICE_ADD_PRECONDITION_SCHEDULE = "add_precondition_schedule"
SERVICE_REMOVE_PRECONDITION_SCHEDULE = "remove_precondition_schedule"


def async_get_device_for_service_call(
    hass: HomeAssistant, call: ServiceCall
) -> dr.DeviceEntry:
    """Get the device entry related to a service call."""
    device_id = call.data[CONF_DEVICE_ID]
    device_registry = dr.async_get(hass)
    if (device_entry := device_registry.async_get(device_id)) is None:
        raise ServiceValidationError(
            translation_domain=DOMAIN,
            translation_key="invalid_device",
            translation_placeholders={"device_id": device_id},
        )

    return device_entry


def async_get_config_for_device(
    hass: HomeAssistant, device_entry: dr.DeviceEntry
) -> ConfigEntry:
    """Get the config entry related to a device entry."""
    config_entry: ConfigEntry
    for entry_id in device_entry.config_entries:
        if entry := hass.config_entries.async_get_entry(entry_id):
            if entry.domain == DOMAIN:
                config_entry = entry
    return config_entry


def async_get_vehicle_for_entry(
    hass: HomeAssistant, device: dr.DeviceEntry, config: ConfigEntry
) -> TeslemetryVehicleData:
    """Get the vehicle data for a config entry."""
    vehicle_data: TeslemetryVehicleData
    assert device.serial_number is not None
    for vehicle in config.runtime_data.vehicles:
        if vehicle.vin == device.serial_number:
            vehicle_data = vehicle
    return vehicle_data


def async_get_energy_site_for_entry(
    hass: HomeAssistant, device: dr.DeviceEntry, config: ConfigEntry
) -> TeslemetryEnergyData:
    """Get the energy site data for a config entry."""
    energy_data: TeslemetryEnergyData
    assert device.serial_number is not None
    for energysite in config.runtime_data.energysites:
        if str(energysite.id) == device.serial_number:
            energy_data = energysite
    return energy_data


@callback
def async_setup_services(hass: HomeAssistant) -> None:
    """Set up the Teslemetry services."""

    async def navigate_gps_request(call: ServiceCall) -> None:
        """Send lat,lon,order with a vehicle."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        await handle_vehicle_command(
            vehicle.api.navigation_gps_request(
                lat=call.data[ATTR_GPS][CONF_LATITUDE],
                lon=call.data[ATTR_GPS][CONF_LONGITUDE],
                order=call.data.get(ATTR_ORDER),
            )
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_NAVIGATE_ATTR_GPS_REQUEST,
        navigate_gps_request,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_GPS): {
                    vol.Required(CONF_LATITUDE): cv.latitude,
                    vol.Required(CONF_LONGITUDE): cv.longitude,
                },
                vol.Optional(ATTR_ORDER): cv.positive_int,
            }
        ),
    )

    async def set_scheduled_charging(call: ServiceCall) -> None:
        """Configure fleet telemetry."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        time: int
        # Convert time to minutes since minute
        if "time" in call.data:
            (hours, minutes, *_seconds) = call.data["time"].split(":")
            time = int(hours) * 60 + int(minutes)
        elif call.data["enable"]:
            raise ServiceValidationError(
                translation_domain=DOMAIN, translation_key="set_scheduled_charging_time"
            )
        else:
            time = 0

        await handle_vehicle_command(
            vehicle.api.set_scheduled_charging(enable=call.data["enable"], time=time)
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_SET_SCHEDULED_CHARGING,
        set_scheduled_charging,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_ENABLE): bool,
                vol.Optional(ATTR_TIME): str,
            }
        ),
    )

    async def set_scheduled_departure(call: ServiceCall) -> None:
        """Configure fleet telemetry."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        enable = call.data.get("enable", True)

        # Preconditioning
        preconditioning_enabled = call.data.get(ATTR_PRECONDITIONING_ENABLED, False)
        preconditioning_weekdays_only = call.data.get(
            ATTR_PRECONDITIONING_WEEKDAYS, False
        )
        departure_time: int | None = None
        if ATTR_DEPARTURE_TIME in call.data:
            (hours, minutes, *_seconds) = call.data[ATTR_DEPARTURE_TIME].split(":")
            departure_time = int(hours) * 60 + int(minutes)
        elif preconditioning_enabled:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="set_scheduled_departure_preconditioning",
            )
        else:
            departure_time = 0

        # Off peak charging
        off_peak_charging_enabled = call.data.get(ATTR_OFF_PEAK_CHARGING_ENABLED, False)
        off_peak_charging_weekdays_only = call.data.get(
            ATTR_OFF_PEAK_CHARGING_WEEKDAYS, False
        )
        end_off_peak_time: int | None = None

        if ATTR_END_OFF_PEAK_TIME in call.data:
            (hours, minutes, *_seconds) = call.data[ATTR_END_OFF_PEAK_TIME].split(":")
            end_off_peak_time = int(hours) * 60 + int(minutes)
        elif off_peak_charging_enabled:
            raise ServiceValidationError(
                translation_domain=DOMAIN,
                translation_key="set_scheduled_departure_off_peak",
            )
        else:
            end_off_peak_time = 0

        await handle_vehicle_command(
            vehicle.api.set_scheduled_departure(
                enable,
                preconditioning_enabled,
                preconditioning_weekdays_only,
                departure_time,
                off_peak_charging_enabled,
                off_peak_charging_weekdays_only,
                end_off_peak_time,
            )
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_SET_SCHEDULED_DEPARTURE,
        set_scheduled_departure,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Optional(ATTR_ENABLE): bool,
                vol.Optional(ATTR_PRECONDITIONING_ENABLED): bool,
                vol.Optional(ATTR_PRECONDITIONING_WEEKDAYS): bool,
                vol.Optional(ATTR_DEPARTURE_TIME): str,
                vol.Optional(ATTR_OFF_PEAK_CHARGING_ENABLED): bool,
                vol.Optional(ATTR_OFF_PEAK_CHARGING_WEEKDAYS): bool,
                vol.Optional(ATTR_END_OFF_PEAK_TIME): str,
            }
        ),
    )

    async def valet_mode(call: ServiceCall) -> None:
        """Configure fleet telemetry."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        await handle_vehicle_command(
            vehicle.api.set_valet_mode(call.data["enable"], call.data["pin"])
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_VALET_MODE,
        valet_mode,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_ENABLE): cv.boolean,
                vol.Required(ATTR_PIN): All(cv.positive_int, Range(min=1000, max=9999)),
            }
        ),
    )

    async def speed_limit(call: ServiceCall) -> None:
        """Configure fleet telemetry."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        enable = call.data["enable"]
        if enable is True:
            await handle_vehicle_command(
                vehicle.api.speed_limit_activate(call.data["pin"])
            )
        elif enable is False:
            await handle_vehicle_command(
                vehicle.api.speed_limit_deactivate(call.data["pin"])
            )

    hass.services.async_register(
        DOMAIN,
        SERVICE_SPEED_LIMIT,
        speed_limit,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_ENABLE): cv.boolean,
                vol.Required(ATTR_PIN): All(cv.positive_int, Range(min=1000, max=9999)),
            }
        ),
    )

    async def time_of_use(call: ServiceCall) -> None:
        """Configure time of use settings."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        site = async_get_energy_site_for_entry(hass, device, config)

        resp = await handle_command(
            site.api.time_of_use_settings(call.data[ATTR_TOU_SETTINGS])
        )
        if "error" in resp:
            raise HomeAssistantError(
                translation_domain=DOMAIN,
                translation_key="command_error",
                translation_placeholders={"error": resp["error"]},
            )

    hass.services.async_register(
        DOMAIN,
        SERVICE_TIME_OF_USE,
        time_of_use,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_TOU_SETTINGS): dict,
            }
        ),
        description_placeholders={
            "time_of_use_url": "https://developer.tesla.com/docs/fleet-api#time_of_use_settings"
        },
    )

    async def add_charge_schedule(call: ServiceCall) -> None:
        """Configure charging schedule for a vehicle."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        # Extract parameters from the service call
        days_of_week = call.data[ATTR_DAYS_OF_WEEK]
        # If days_of_week is a list (from select with multiple), convert to comma-separated string
        if isinstance(days_of_week, list):
            days_of_week = ",".join(days_of_week)
        enabled = call.data[ATTR_ENABLE]

        # Optional parameters
        location = call.data.get(
            ATTR_LOCATION,
            {
                CONF_LATITUDE: hass.config.latitude,
                CONF_LONGITUDE: hass.config.longitude,
            },
        )

        # Handle time inputs
        start_time = None
        if start_time_obj := call.data.get(ATTR_START_TIME):
            # Convert time object to minutes since midnight
            start_time = start_time_obj.hour * 60 + start_time_obj.minute

        end_time = None
        if end_time_obj := call.data.get(ATTR_END_TIME):
            # Convert time object to minutes since midnight
            end_time = end_time_obj.hour * 60 + end_time_obj.minute

        one_time = call.data.get(ATTR_ONE_TIME)
        schedule_id = call.data.get(ATTR_ID)
        name = call.data.get(ATTR_NAME)

        await handle_vehicle_command(
            vehicle.api.add_charge_schedule(
                days_of_week=days_of_week,
                enabled=enabled,
                lat=location[CONF_LATITUDE],
                lon=location[CONF_LONGITUDE],
                start_time=start_time,
                end_time=end_time,
                one_time=one_time,
                id=schedule_id,
                name=name,
            )
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_ADD_CHARGE_SCHEDULE,
        add_charge_schedule,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_DAYS_OF_WEEK): cv.ensure_list,
                vol.Required(ATTR_ENABLE): cv.boolean,
                vol.Optional(ATTR_LOCATION): {
                    vol.Required(CONF_LATITUDE): cv.latitude,
                    vol.Required(CONF_LONGITUDE): cv.longitude,
                },
                vol.Optional(ATTR_START_TIME): cv.time,
                vol.Optional(ATTR_END_TIME): cv.time,
                vol.Optional(ATTR_ONE_TIME): cv.boolean,
                vol.Optional(ATTR_ID): cv.positive_int,
                vol.Optional(ATTR_NAME): cv.string,
            }
        ),
    )

    async def remove_charge_schedule(call: ServiceCall) -> None:
        """Remove a charging schedule for a vehicle."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        # Extract parameters from the service call
        schedule_id = call.data[ATTR_ID]

        await handle_vehicle_command(
            vehicle.api.remove_charge_schedule(
                id=schedule_id,
            )
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_REMOVE_CHARGE_SCHEDULE,
        remove_charge_schedule,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_ID): cv.positive_int,
            }
        ),
    )

    async def add_precondition_schedule(call: ServiceCall) -> None:
        """Add or modify a precondition schedule for a vehicle."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        # Extract parameters from the service call
        days_of_week = call.data[ATTR_DAYS_OF_WEEK]
        # If days_of_week is a list (from select with multiple), convert to comma-separated string
        if isinstance(days_of_week, list):
            days_of_week = ",".join(days_of_week)
        enabled = call.data[ATTR_ENABLE]
        location = call.data.get(
            ATTR_LOCATION,
            {
                CONF_LATITUDE: hass.config.latitude,
                CONF_LONGITUDE: hass.config.longitude,
            },
        )

        # Convert time object to minutes since midnight
        precondition_time = (
            call.data[ATTR_PRECONDITION_TIME].hour * 60
            + call.data[ATTR_PRECONDITION_TIME].minute
        )

        # Optional parameters
        schedule_id = call.data.get(ATTR_ID)
        one_time = call.data.get(ATTR_ONE_TIME)
        name = call.data.get(ATTR_NAME)

        await handle_vehicle_command(
            vehicle.api.add_precondition_schedule(
                days_of_week=days_of_week,
                enabled=enabled,
                lat=location[CONF_LATITUDE],
                lon=location[CONF_LONGITUDE],
                precondition_time=precondition_time,
                id=schedule_id,
                one_time=one_time,
                name=name,
            )
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_ADD_PRECONDITION_SCHEDULE,
        add_precondition_schedule,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_DAYS_OF_WEEK): cv.ensure_list,
                vol.Required(ATTR_ENABLE): cv.boolean,
                vol.Optional(ATTR_LOCATION): {
                    vol.Required(CONF_LATITUDE): cv.latitude,
                    vol.Required(CONF_LONGITUDE): cv.longitude,
                },
                vol.Required(ATTR_PRECONDITION_TIME): cv.time,
                vol.Optional(ATTR_ID): cv.positive_int,
                vol.Optional(ATTR_ONE_TIME): cv.boolean,
                vol.Optional(ATTR_NAME): cv.string,
            }
        ),
    )

    async def remove_precondition_schedule(call: ServiceCall) -> None:
        """Remove a preconditioning schedule for a vehicle."""
        device = async_get_device_for_service_call(hass, call)
        config = async_get_config_for_device(hass, device)
        vehicle = async_get_vehicle_for_entry(hass, device, config)

        # Extract parameters from the service call
        schedule_id = call.data[ATTR_ID]

        await handle_vehicle_command(
            vehicle.api.remove_precondition_schedule(
                id=schedule_id,
            )
        )

    hass.services.async_register(
        DOMAIN,
        SERVICE_REMOVE_PRECONDITION_SCHEDULE,
        remove_precondition_schedule,
        schema=vol.Schema(
            {
                vol.Required(CONF_DEVICE_ID): cv.string,
                vol.Required(ATTR_ID): cv.positive_int,
            }
        ),
    )
