package org.github.tess1o.geopulse.gps.mapper;

import jakarta.enterprise.context.ApplicationScoped;
import org.github.tess1o.geopulse.gps.integrations.colota.model.ColotaLocationMessage;
import org.github.tess1o.geopulse.gps.integrations.dawarich.model.point.DawarichLocation;
import org.github.tess1o.geopulse.gps.integrations.homeassistant.model.HomeAssistantGpsData;
import org.github.tess1o.geopulse.gps.integrations.homeassistant.model.HomeAssistantLocation;
import org.github.tess1o.geopulse.gps.integrations.traccar.model.TraccarDevice;
import org.github.tess1o.geopulse.gps.integrations.traccar.model.TraccarPosition;
import org.github.tess1o.geopulse.gps.integrations.traccar.model.TraccarPositionData;
import org.github.tess1o.geopulse.gps.model.GpsPointEntity;
import org.github.tess1o.geopulse.gps.model.GpsPointPathPointDTO;
import org.github.tess1o.geopulse.gps.model.GpsPointDTO;
import org.github.tess1o.geopulse.gps.model.RawGpsPointMapPointDTO;
import org.github.tess1o.geopulse.shared.gps.GpsSourceType;
import org.github.tess1o.geopulse.gps.integrations.overland.model.OverlandLocationMessage;
import org.github.tess1o.geopulse.gps.integrations.owntracks.model.OwnTracksLocationMessage;
import org.github.tess1o.geopulse.shared.geo.GeoUtils;
import org.github.tess1o.geopulse.user.model.UserEntity;

import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;

@ApplicationScoped
public class GpsPointMapper {

    public GpsPointEntity toEntity(OwnTracksLocationMessage message, UserEntity userId, String deviceId, GpsSourceType sourceType) {
        String device = deviceId != null ? deviceId : "Unknown Device";
        GpsPointEntity entity = new GpsPointEntity();
        entity.setDeviceId(device);
        entity.setUser(userId);
        entity.setCoordinates(GeoUtils.createPoint(message.getLon(), message.getLat()));
        entity.setTimestamp(Instant.ofEpochSecond(message.getTst()));
        entity.setAccuracy(message.getAcc());
        entity.setBattery(message.getBatt());
        // OwnTracks reports km/h, but GPSLogger sends OwnTracks-shaped payloads with speed in m/s.
        Double velocity = message.getVel();
        if (velocity != null && sourceType == GpsSourceType.GPSLOGGER) {
            velocity = velocity * 3.6;
        }
        entity.setVelocity(velocity);
        entity.setAltitude(message.getAlt());
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());
        if ((sourceType == GpsSourceType.OWNTRACKS || sourceType == GpsSourceType.GPSLOGGER)
                && message.getExt() != null && !message.getExt().isEmpty()) {
            entity.setTelemetry(new LinkedHashMap<>(message.getExt()));
        } else {
            entity.setTelemetry(null);
        }

        return entity;
    }

    public GpsPointEntity toEntity(OverlandLocationMessage message, UserEntity userId, GpsSourceType sourceType) {
        GpsPointEntity entity = new GpsPointEntity();
        entity.setDeviceId(message.getProperties().getDeviceId());
        entity.setUser(userId);
        entity.setCoordinates(GeoUtils.createPoint(message.getGeometry().getCoordinates()[0], message.getGeometry().getCoordinates()[1]));
        entity.setTimestamp(message.getProperties().getTimestamp());
        entity.setAccuracy(message.getProperties().getHorizontalAccuracy());
        entity.setBattery(message.getProperties().getBatteryLevel() * 100);
        // Convert speed from m/s to km/h for consistent storage (Overland sends m/s)
        Double speedMs = message.getProperties().getSpeed();
        entity.setVelocity(speedMs != null ? speedMs * 3.6 : null);
        entity.setAltitude(message.getProperties().getAltitude() * 1.0);
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());

        return entity;
    }

    public GpsPointEntity toEntity(DawarichLocation message, UserEntity userId, GpsSourceType sourceType) {
        GpsPointEntity entity = new GpsPointEntity();
        entity.setDeviceId(message.getProperties().getDeviceId());
        entity.setUser(userId);
        entity.setCoordinates(GeoUtils.createPoint(message.getGeometry().getLongitude(), message.getGeometry().getLatitude()));
        entity.setTimestamp(message.getProperties().getTimestamp());
        entity.setAccuracy(message.getProperties().getVerticalAccuracy());
        // Convert speed from m/s to km/h for consistent storage (Dawarich sends m/s)
        Double speedMs = message.getProperties().getSpeed();
        entity.setVelocity(speedMs != null ? speedMs * 3.6 : null);
        entity.setAltitude(Math.round(message.getProperties().getAltitude()) * 1.0);
        entity.setBattery(-1.0);
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());


        return entity;
    }

    public GpsPointEntity toEntity(HomeAssistantGpsData message, UserEntity userId, GpsSourceType sourceType) {
        GpsPointEntity entity = new GpsPointEntity();
        entity.setUser(userId);
        entity.setTimestamp(message.getTimestamp() != null ? message.getTimestamp() : Instant.now());
        entity.setDeviceId(message.getDeviceId());

        HomeAssistantLocation location = message.getLocation();

        entity.setCoordinates(GeoUtils.createPoint(location.getLongitude(), location.getLatitude()));
        entity.setAccuracy(location.getAccuracy());
        // Convert speed from m/s to km/h for consistent storage (HomeAssistant sends m/s)
        Double speedMs = location.getSpeed();
        entity.setVelocity(speedMs != null ? speedMs * 3.6 : null);
        entity.setAltitude(Math.round(location.getAltitude()) * 1.0);
        entity.setBattery(message.getBattery().getLevel() * 1.0);
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());


        return entity;
    }

    public GpsPointEntity toEntity(ColotaLocationMessage message, UserEntity userId, GpsSourceType sourceType) {
        GpsPointEntity entity = new GpsPointEntity();
        entity.setDeviceId("Colota");
        entity.setUser(userId);
        entity.setCoordinates(GeoUtils.createPoint(message.getLon(), message.getLat()));
        entity.setTimestamp(Instant.ofEpochSecond(message.getTst()));
        entity.setAccuracy(message.getAcc());
        entity.setBattery(message.getBatt());
        // Convert speed from m/s to km/h for consistent storage (Colota sends m/s)
        Double speedMs = message.getVel();
        entity.setVelocity(speedMs != null ? speedMs * 3.6 : null);
        entity.setAltitude(message.getAlt());
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());

        return entity;
    }

    public GpsPointEntity toEntity(TraccarPositionData message, UserEntity userId, GpsSourceType sourceType) {
        TraccarPosition position = message.getPosition();
        TraccarDevice device = message.getDevice();

        GpsPointEntity entity = new GpsPointEntity();
        entity.setDeviceId(resolveDeviceId(device, position));
        entity.setUser(userId);
        entity.setCoordinates(GeoUtils.createPoint(position.getLongitude(), position.getLatitude()));
        entity.setTimestamp(position.resolveTimestamp());
        entity.setAccuracy(position.getAccuracy());
        entity.setBattery(position.resolveBatteryLevel());

        // Traccar Position model stores speed in knots.
        Double speedKnots = position.getSpeed();
        entity.setVelocity(speedKnots != null ? speedKnots * 1.852 : null);

        entity.setAltitude(position.getAltitude());
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());

        return entity;
    }

    public GpsPointEntity toEntity(GpsPointDTO message, String deviceId, UserEntity userId, GpsSourceType sourceType) {
        GpsPointEntity entity = new GpsPointEntity();
        entity.setDeviceId(deviceId);
        entity.setUser(userId);
        entity.setCoordinates(GeoUtils.createPoint(message.getCoordinates().getLng(), message.getCoordinates().getLat()));
        entity.setTimestamp(message.getTimestamp() != null ? message.getTimestamp() : Instant.now());
        entity.setAccuracy(message.getAccuracy());
        entity.setBattery(message.getBattery());
        // Mobile app submits speed in m/s; store in km/h for consistency.
        Double speedMs = message.getVelocity();
        entity.setVelocity(speedMs != null ? speedMs * 3.6 : null);
        entity.setAltitude(message.getAltitude());
        entity.setSourceType(sourceType);
        entity.setCreatedAt(Instant.now());

        return entity;
    }

    private String resolveDeviceId(TraccarDevice device, TraccarPosition position) {
        if (device != null) {
            if (device.getUniqueId() != null && !device.getUniqueId().isBlank()) {
                return device.getUniqueId();
            }
            if (device.getName() != null && !device.getName().isBlank()) {
                return device.getName();
            }
            if (device.getId() != null) {
                return "traccar-" + device.getId();
            }
        }
        if (position != null && position.getDeviceId() != null) {
            return "traccar-" + position.getDeviceId();
        }
        return "Traccar";
    }

    /**
     * Convert a GpsPointEntity to a GpsPointPathPointDTO.
     *
     * @param entity The GPS point entity
     * @return The GPS point path point DTO
     */
    public GpsPointPathPointDTO toPathPoint(GpsPointEntity entity) {
        if (entity == null || entity.getCoordinates() == null) {
            return null;
        }

        return new GpsPointPathPointDTO(
                entity.getId(),
                entity.getCoordinates().getX(), // Longitude
                entity.getCoordinates().getY(), // Latitude
                entity.getTimestamp(),
                entity.getAccuracy(),
                entity.getAltitude(),
                entity.getVelocity(),
                entity.getUser().getId(),
                entity.getSourceType().name(),
                null,
                null
        );
    }

    /**
     * Convert a list of GpsPointEntity objects to a list of GpsPointPathPointDTO objects.
     *
     * @param entities The list of GPS point entities
     * @return The list of GPS point path point DTOs
     */
    public List<GpsPointPathPointDTO> toPathPoints(List<GpsPointEntity> entities) {
        if (entities == null) {
            return List.of();
        }

        return entities.stream()
                .map(this::toPathPoint)
                .collect(Collectors.toList());
    }

    /**
     * Convert a GpsPointEntity to an OwnTracksLocationMessage.
     *
     * @param entity The GPS point entity
     * @return The OwnTracks location message
     */
    public OwnTracksLocationMessage toOwnTracksLocationMessage(GpsPointEntity entity) {
        if (entity == null || entity.getCoordinates() == null) {
            return null;
        }

        return OwnTracksLocationMessage.builder()
                .lat(entity.getLatitude())
                .lon(entity.getLongitude())
                .tst(entity.getTimestamp().getEpochSecond())
                .acc(entity.getAccuracy())
                .batt(entity.getBattery())
                .vel(entity.getVelocity())
                .alt(entity.getAltitude())
                .type("location")
                .tid(entity.getDeviceId())
                .ext(entity.getTelemetry())
                .createdAt(entity.getCreatedAt().getEpochSecond())
                .build();
    }

    /**
     * Convert a list of GpsPointEntity objects to a list of OwnTracksLocationMessage objects.
     *
     * @param entities The list of GPS point entities
     * @return The list of OwnTracks location messages
     */
    public List<OwnTracksLocationMessage> toOwnTracksLocationMessages(List<GpsPointEntity> entities) {
        if (entities == null) {
            return List.of();
        }

        return entities.stream()
                .map(this::toOwnTracksLocationMessage)
                .collect(Collectors.toList());
    }

    /**
     * Convert a GpsPointEntity to a GpsPointDTO.
     *
     * @param entity The GPS point entity
     * @return The GPS point DTO
     */
    public GpsPointDTO toGpsPointDTO(GpsPointEntity entity) {
        if (entity == null || entity.getCoordinates() == null) {
            return null;
        }

        GpsPointDTO.CoordinatesDTO coordinates = new GpsPointDTO.CoordinatesDTO(
                entity.getCoordinates().getY(), // Latitude
                entity.getCoordinates().getX()  // Longitude
        );

        return new GpsPointDTO(
                entity.getId(),
                entity.getTimestamp(),
                coordinates,
                entity.getAccuracy(),
                entity.getBattery(),
                entity.getVelocity(),
                entity.getAltitude(),
                entity.getSourceType().name(),
                null,
                null
        );
    }

    public RawGpsPointMapPointDTO toRawGpsPointMapPointDTO(GpsPointEntity entity) {
        if (entity == null || entity.getCoordinates() == null) {
            return null;
        }

        return RawGpsPointMapPointDTO.builder()
                .id(entity.getId())
                .timestamp(entity.getTimestamp())
                .latitude(entity.getCoordinates().getY())
                .longitude(entity.getCoordinates().getX())
                .accuracy(entity.getAccuracy())
                .battery(entity.getBattery())
                .velocity(entity.getVelocity())
                .altitude(entity.getAltitude())
                .sourceType(entity.getSourceType() == null ? null : entity.getSourceType().name())
                .build();
    }

    public List<RawGpsPointMapPointDTO> toRawGpsPointMapPointDTOs(List<GpsPointEntity> entities) {
        if (entities == null) {
            return List.of();
        }

        return entities.stream()
                .map(this::toRawGpsPointMapPointDTO)
                .collect(Collectors.toList());
    }

    /**
     * Convert a list of GpsPointEntity objects to a list of GpsPointDTO objects.
     *
     * @param entities The list of GPS point entities
     * @return The list of GPS point DTOs
     */
    public List<GpsPointDTO> toGpsPointDTOs(List<GpsPointEntity> entities) {
        if (entities == null) {
            return List.of();
        }

        return entities.stream()
                .map(this::toGpsPointDTO)
                .collect(Collectors.toList());
    }
}
