package com.grigowashere.aismap.ble.hub;

import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.AISNavigationAid;
import com.grigowashere.aismap.models.Vessel;

import org.json.JSONObject;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

/**
 * Maps loose JSON objects from ais_hub into app models (defensive keys).
 */
public final class AisHubJsonMapper {

    private AisHubJsonMapper() {}

    public static String mmsiString(JSONObject o) {
        if (o == null) return null;
        if (o.has("mmsi")) {
            Object v = o.opt("mmsi");
            if (v instanceof Number) return String.valueOf(((Number) v).longValue());
            String s = o.optString("mmsi", null);
            return s != null && !s.isEmpty() ? s : null;
        }
        return null;
    }

    /**
     * Returns `o.dynamic` if it exists (AIS Hub v2 nests position/motion there
     * for target.update and vessel snapshot items), otherwise returns `o` itself
     * (ownship.update keeps lat/lon at root).
     */
    private static JSONObject dyn(JSONObject o) {
        if (o == null) return null;
        JSONObject d = o.optJSONObject("dynamic");
        return d != null ? d : o;
    }

    public static double optLat(JSONObject o) {
        if (o == null) return Double.NaN;
        JSONObject src = dyn(o);
        if (src.has("latitude")) return src.optDouble("latitude", Double.NaN);
        if (src.has("lat")) return src.optDouble("lat", Double.NaN);
        return Double.NaN;
    }

    public static double optLon(JSONObject o) {
        if (o == null) return Double.NaN;
        JSONObject src = dyn(o);
        if (src.has("longitude")) return src.optDouble("longitude", Double.NaN);
        if (src.has("lon")) return src.optDouble("lon", Double.NaN);
        return Double.NaN;
    }

    public static double optCourse(JSONObject o) {
        if (o == null) return Double.NaN;
        JSONObject src = dyn(o);
        double v = src.optDouble("cog", Double.NaN);
        if (!Double.isNaN(v)) return v;
        v = src.optDouble("course", Double.NaN);
        if (!Double.isNaN(v)) return v;
        v = src.optDouble("true_course", Double.NaN);
        if (!Double.isNaN(v)) return v;
        return src.optDouble("heading", Double.NaN);
    }

    public static double optSpeed(JSONObject o) {
        if (o == null) return Double.NaN;
        JSONObject src = dyn(o);
        double v = src.optDouble("sog", Double.NaN);
        if (!Double.isNaN(v)) return v;
        v = src.optDouble("speed", Double.NaN);
        if (!Double.isNaN(v)) return v;
        return src.optDouble("stw", Double.NaN);
    }

    public static double optHeading(JSONObject o) {
        if (o == null) return Double.NaN;
        JSONObject src = dyn(o);
        double v = src.optDouble("heading", Double.NaN);
        if (!Double.isNaN(v)) return v;
        return src.optDouble("hdg", Double.NaN);
    }

    /**
     * Returns a valid epoch-seconds timestamp taken from any of the known fields,
     * or NaN if none present. Supports ts, last_dynamic_ts, last_seen, last_static_ts.
     */
    private static double pickEpochSeconds(JSONObject o) {
        if (o == null) return Double.NaN;
        String[] keys = {"ts", "last_dynamic_ts", "last_seen", "last_static_ts"};
        for (String k : keys) {
            if (!o.has(k)) continue;
            double ts = o.optDouble(k, Double.NaN);
            if (Double.isNaN(ts) || ts <= 0) continue;
            if (ts > 1e12) ts /= 1000.0;
            if (ts > 946684800) return ts;
        }
        return Double.NaN;
    }

    /**
     * Converts a server-epoch timestamp to a device-local {@link LocalDateTime}
     * using {@link HubTimeSync} so that stale checks (which compare against
     * {@code LocalDateTime.now()}) are immune to hub clock skew.
     */
    private static LocalDateTime deviceLocalFromServerEpoch(double serverSec) {
        double deviceSec = HubTimeSync.toDeviceEpochSeconds(serverSec);
        long secs = (long) deviceSec;
        int nanos = (int) ((deviceSec - secs) * 1e9);
        if (nanos < 0) { secs -= 1; nanos += 1_000_000_000; }
        return LocalDateTime.ofInstant(Instant.ofEpochSecond(secs, nanos), ZoneId.systemDefault());
    }

    public static void applyTimestampFromJson(JSONObject o, AISVessel vessel) {
        double ts = pickEpochSeconds(o);
        if (!Double.isNaN(ts)) {
            vessel.setLastUpdate(deviceLocalFromServerEpoch(ts));
            return;
        }
        if (o != null && o.has("last_update")) {
            try {
                String s = o.optString("last_update", "");
                if (!s.isEmpty()) {
                    vessel.setLastUpdate(LocalDateTime.parse(s));
                    return;
                }
            } catch (Exception ignore) {}
        }
        vessel.setLastUpdate(LocalDateTime.now());
    }

    public static void applyTimestampFromJson(JSONObject o, AISNavigationAid aid) {
        double ts = pickEpochSeconds(o);
        if (!Double.isNaN(ts)) {
            aid.setLastUpdate(deviceLocalFromServerEpoch(ts));
            return;
        }
        aid.setLastUpdate(LocalDateTime.now());
    }

    public static void applyTimestampFromJson(JSONObject o, Vessel vessel) {
        double ts = pickEpochSeconds(o);
        if (!Double.isNaN(ts)) {
            vessel.setLastUpdate(deviceLocalFromServerEpoch(ts));
            return;
        }
        vessel.setLastUpdate(LocalDateTime.now());
    }

    public static AISVessel aisVesselFromJson(JSONObject o) {
        if (o == null) return null;
        String mmsi = mmsiString(o);
        if (mmsi == null) return null;
        AISVessel v = new AISVessel(mmsi);
        String name = o.optString("name", o.optString("vessel_name", o.optString("shipname", "")));
        if (!name.isEmpty()) v.setVesselName(name);
        v.setCallSign(o.optString("call_sign", o.optString("callsign", "")));
        double lat = optLat(o);
        double lon = optLon(o);
        double cog = optCourse(o);
        double sog = optSpeed(o);
        if (!Double.isNaN(lat) && !Double.isNaN(lon)
                && !Double.isNaN(cog) && !Double.isNaN(sog)) {
            v.updatePosition(lat, lon, cog, sog);
        } else {
            if (!Double.isNaN(lat) && !Double.isNaN(lon)) {
                v.setLatitude(lat);
                v.setLongitude(lon);
            }
            if (!Double.isNaN(cog)) v.setCourse(cog);
            if (!Double.isNaN(sog)) v.setSpeed(sog);
        }
        double hdg = optHeading(o);
        if (!Double.isNaN(hdg)) v.setHeading(hdg);
        String cls = o.optString("class", o.optString("vessel_class", o.optString("ship_class", "")));
        if (!cls.isEmpty()) v.setVesselClass(cls);
        int imo = o.optInt("imo", 0);
        if (imo > 0) v.setImo(imo);
        int shipType = o.optInt("ship_type", -1);
        if (shipType >= 0) v.setVesselType(shipTypeName(shipType));
        JSONObject dims = o.optJSONObject("dims");
        if (dims != null) {
            double a = dims.optDouble("a", 0.0);
            double b = dims.optDouble("b", 0.0);
            double c = dims.optDouble("c", 0.0);
            double d = dims.optDouble("d", 0.0);
            if (a + b > 0.0) v.setLength(a + b);
            if (c + d > 0.0) v.setWidth(c + d);
        }
        JSONObject voyage = o.optJSONObject("voyage");
        if (voyage != null) {
            double draft = voyage.optDouble("draught", Double.NaN);
            if (!Double.isNaN(draft) && draft > 0.0) v.setDraft(draft);
            String destination = voyage.optString("destination", "");
            if (!destination.isEmpty()) v.setDestination(destination);
        }
        JSONObject dynamic = o.optJSONObject("dynamic");
        if (dynamic != null) {
            if (dynamic.has("nav_status")) {
                v.setNavigationalStatus(String.valueOf(dynamic.optInt("nav_status")));
            }
            double rot = dynamic.optDouble("rot", Double.NaN);
            if (!Double.isNaN(rot)) v.setRateOfTurn(rot);
        }
        JSONObject signal = o.optJSONObject("signal");
        if (signal != null) {
            double db = signal.optDouble("last_db", Double.NaN);
            if (!Double.isNaN(db)) v.setSignalStrength((int) Math.round(db));
        }
        if (o.has("position_accuracy")) {
            v.setPositionAccuracy(o.optBoolean("position_accuracy", false));
        }
        applyTimestampFromJson(o, v);
        return v;
    }

    public static AISVessel baseStationFromJson(JSONObject o) {
        if (o == null) return null;
        String mmsi = mmsiString(o);
        if (mmsi == null) return null;
        AISVessel v = new AISVessel(mmsi);
        v.setVesselClass("Base Station");
        v.setVesselType("Base Station");
        v.setVesselName("Base Station " + mmsi);
        double lat = optLat(o);
        double lon = optLon(o);
        if (!Double.isNaN(lat) && !Double.isNaN(lon)) {
            v.setLatitude(lat);
            v.setLongitude(lon);
        }
        if (o.has("accuracy")) {
            v.setPositionAccuracy(o.optBoolean("accuracy", false));
        }
        if (o.has("epfd")) {
            v.setDestination("EPFD: " + o.optInt("epfd"));
        }
        applyTimestampFromJson(o, v);
        return v;
    }

    public static AISNavigationAid navigationAidFromJson(JSONObject o) {
        if (o == null) return null;
        String mmsi = mmsiString(o);
        if (mmsi == null) return null;
        AISNavigationAid aid = new AISNavigationAid(mmsi);
        aid.setAidName(o.optString("name", "AtoN " + mmsi));
        aid.setAidType(o.optInt("type", 0));
        double lat = optLat(o);
        double lon = optLon(o);
        if (!Double.isNaN(lat) && !Double.isNaN(lon)) {
            aid.setLatitude(lat);
            aid.setLongitude(lon);
        }
        if (o.has("accuracy")) {
            aid.setPositionAccuracy(o.optBoolean("accuracy", false));
        }
        if (o.has("virtual")) {
            aid.setOffPositionIndicator(o.optBoolean("virtual", false));
        }
        applyTimestampFromJson(o, aid);
        return aid;
    }

    public static void mergeVesselOwnship(JSONObject o, Vessel v) {
        if (o == null || v == null) return;
        String mmsi = mmsiString(o);
        if (mmsi != null) v.setMmsi(mmsi);
        String name = o.optString("name", o.optString("vessel_name", ""));
        if (!name.isEmpty()) v.setVesselName(name);
        String cs = o.optString("call_sign", o.optString("callsign", ""));
        if (!cs.isEmpty()) v.setCallSign(cs);
        double lat = optLat(o);
        double lon = optLon(o);
        if (!Double.isNaN(lat) && !Double.isNaN(lon)) {
            v.setLatitude(lat);
            v.setLongitude(lon);
        }
        double cog = optCourse(o);
        if (!Double.isNaN(cog)) v.setCourse(cog);
        double sog = optSpeed(o);
        if (!Double.isNaN(sog)) v.setSpeed(sog);
        double hdg = optHeading(o);
        if (!Double.isNaN(hdg)) v.setHeading(hdg);
        v.setFixTime(System.currentTimeMillis());
        v.setFixQuality("HUB");
        if (o.has("accuracy") || o.has("h_acc")) {
            float acc = (float) o.optDouble("accuracy", o.optDouble("h_acc", -1));
            if (acc >= 0) v.setAccuracy(acc);
        }
        applyTimestampFromJson(o, v);
    }

    private static String shipTypeName(int code) {
        if (code >= 70 && code <= 79) return "Cargo";
        if (code >= 80 && code <= 89) return "Tanker";
        if (code == 30) return "Fishing";
        if (code >= 60 && code <= 69) return "Passenger";
        if (code == 36 || code == 37) return "Sailing/Pleasure";
        if (code == 31 || code == 32 || code == 52) return "Tug";
        if (code == 35) return "Military";
        return "Other";
    }
}
