package com.wytehat.btlogger;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import java.util.HashMap;
import java.util.List;

public class BluetoothTrackingService extends Service implements LocationListener {
    public static final String ACTION_DATA_CHANGED = "com.wytehat.btlogger.DATA_CHANGED";
    public static final String ACTION_CONFIG_CHANGED = "com.wytehat.btlogger.CONFIG_CHANGED";
    public static final String ACTION_RANGE_MODE = "com.wytehat.btlogger.RANGE_MODE";
    public static final String ACTION_ACK_LEFT_BEHIND = "com.wytehat.btlogger.ACK_LEFT_BEHIND";
    public static final String EXTRA_ADDRESS = "address";
    /** How often an unacknowledged left-behind item re-alerts. */
    private static final long ALERT_REPEAT_MS = 60000L;
    private static final int NOTIFICATION_ID = 42;
    private static final String CHANNEL_ID = "bluetooth_tracking";
    private static final String ALERT_CHANNEL_ID = "tracked_item_disconnects";
    private TrackerDatabase db;
    private LocationManager locationManager;
    private BluetoothAdapter bluetooth;
    private Location lastLocation;
    private boolean receiverRegistered;
    private boolean rangeModeActive;
    private int phoneBattery = 100;
    private final Handler handler = new Handler();
    private final HashMap<String, Long> lastPeriodicLog = new HashMap<String, Long>();
    /** Items reported left behind, address -> display name, until acknowledged. */
    private final HashMap<String, String> leftBehind = new HashMap<String, String>();
    private boolean alertLoopRunning;

    private final BroadcastReceiver receiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_RANGE_MODE.equals(action)) {
                rangeModeActive = intent.getBooleanExtra("active", false);
                return;
            }
            if (ACTION_ACK_LEFT_BEHIND.equals(action)) {
                acknowledgeLeftBehind(intent.getStringExtra(EXTRA_ADDRESS));
                return;
            }
            if (ACTION_CONFIG_CHANGED.equals(action)) {
                restartBackgroundScan();
                configureLocation();
                return;
            }
            if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
                int level = intent.getIntExtra("level", 100);
                int scale = intent.getIntExtra("scale", 100);
                phoneBattery = scale > 0 ? level * 100 / scale : 100;
                configureLocation();
                return;
            }
            if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
                int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
											   BluetoothAdapter.ERROR);
                if (state == BluetoothAdapter.STATE_OFF ||
                    state == BluetoothAdapter.STATE_TURNING_OFF)
                    disconnectAll("Manual / Intentional Disconnect");
                return;
            }

            BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(
                BluetoothDevice.EXTRA_DEVICE);
            if (device == null) return;
            String address = address(device);
            if (address == null) return;
            String broadcastName = name(device);
            long now = System.currentTimeMillis();
            TrackerDatabase.Fix fix = currentFix();
            resolveVendor(address, -1);

            if (BluetoothDevice.ACTION_ACL_CONNECTED.equals(action)) {
                db.connected(address, broadcastName, now, fix);
                captureFresh(address, now);
                configureLocation();
            } else if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) {
                DeviceRecord previous = db.get(address);
                db.disconnected(address, broadcastName, now, fix, infer(previous));
                if (previous != null && previous.tracked == 1 && previous.connected == 1)
                    markLeftBehind(previous, infer(previous));
                captureFresh(address, now);
                configureLocation();
            } else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
                if (rssi != Short.MIN_VALUE) {
                    DeviceRecord before = db.get(address);
                    boolean firstFreshSight = before != null && before.trackBroadcast == 1 &&
                        (before.lastRssiAt <= 0 || now - before.lastRssiAt > 180000L);
                    db.telemetry(address, broadcastName, null, Integer.valueOf(rssi), now, null);
                    db.recordObservation(address, now, rssi, broadcastName, db.lookupOui(address),
										 null, null, -1, -1, null, fix);
                    if (firstFreshSight) { captureFresh(address, now); configureLocation(); }
                }
            } else if ("android.bluetooth.device.action.BATTERY_LEVEL_CHANGED".equals(action)) {
                int battery = intent.getIntExtra(
                    "android.bluetooth.device.extra.BATTERY_LEVEL", -1);
                if (battery >= 0 && battery <= 100)
                    db.telemetry(address, broadcastName, Integer.valueOf(battery), null, now, fix);
            } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
                int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
											   BluetoothDevice.ERROR);
                if (state == BluetoothDevice.BOND_BONDED)
                    db.markTracked(address, broadcastName);
                else if (state == BluetoothDevice.BOND_NONE)
                    db.disconnected(address, broadcastName, now, fix,
									"Manual / Intentional Disconnect");
            }
            resolveVendor(address, -1);
            notifyChanged();
        }
    };

    private final BluetoothAdapter.LeScanCallback leScanCallback =
	new BluetoothAdapter.LeScanCallback() {
		public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
			if (device == null) return;
			String address = address(device);
			if (address == null) return;
			long now = System.currentTimeMillis();
			DeviceRecord before = db.get(address);
			boolean firstFreshSight = before != null && before.trackBroadcast == 1 &&
				(before.lastRssiAt <= 0 || now - before.lastRssiAt > 180000L);
			db.telemetry(address, name(device), null, Integer.valueOf(rssi), now, null);
			if (firstFreshSight) { captureFresh(address, now); configureLocation(); }
			int companyId = parseCompanyId(scanRecord);
			resolveVendor(address, companyId);
			String vendor = companyId >= 0 ? db.lookupCompany(companyId) : db.lookupOui(address);
			db.recordObservation(address, now, rssi, name(device), vendor, null, null,
								 -1, companyId, bytesToHex(scanRecord), currentFix());
			notifyChanged();
		}
	};

    private final Runnable scanTask = new Runnable() {
        public void run() {
            try {
                if (hasBluetoothPermission() && bluetooth != null && bluetooth.isEnabled() &&
                    shouldBackgroundScan() && !rangeModeActive) {
                    if (!bluetooth.isDiscovering()) bluetooth.startDiscovery();
                    bluetooth.startLeScan(leScanCallback);
                    handler.postDelayed(new Runnable() {
							public void run() {
								try { bluetooth.stopLeScan(leScanCallback); }
								catch (Exception ignored) { }
								configureLocation();
							}
						}, 12000L);
                }
            } catch (Exception ignored) { }
            handler.postDelayed(this, 90000L);
        }
    };

    public void onCreate() {
        super.onCreate();
        db = new TrackerDatabase(this);
        bluetooth = BluetoothAdapter.getDefaultAdapter();
        startForeground(NOTIFICATION_ID, buildNotification());
        syncPairedDevices();
        notifyChanged();
        IntentFilter filter = new IntentFilter();
        filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
        filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
        filter.addAction(BluetoothDevice.ACTION_FOUND);
        filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
        filter.addAction("android.bluetooth.device.action.BATTERY_LEVEL_CHANGED");
        filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
        filter.addAction(Intent.ACTION_BATTERY_CHANGED);
        filter.addAction(ACTION_CONFIG_CHANGED);
        filter.addAction(ACTION_RANGE_MODE);
        filter.addAction(ACTION_ACK_LEFT_BEHIND);
        registerReceiver(receiver, filter);
        receiverRegistered = true;
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        loadLastLocation();
        configureLocation();
        handler.post(scanTask);
    }

    public int onStartCommand(Intent intent, int flags, int startId) {
        configureLocation();
        return START_STICKY;
    }

    public IBinder onBind(Intent intent) { return null; }

    public void onDestroy() {
        handler.removeCallbacks(scanTask);
        handler.removeCallbacks(alertTask);
        try { if (bluetooth != null) bluetooth.stopLeScan(leScanCallback); }
        catch (Exception ignored) { }
        if (receiverRegistered) unregisterReceiver(receiver);
        stopLocation();
        db.close();
        super.onDestroy();
    }

    private void loadLastLocation() {
        if (!hasLocationPermission()) return;
        try {
            lastLocation = newer(locationManager.getLastKnownLocation(
									 LocationManager.GPS_PROVIDER), locationManager.getLastKnownLocation(
									 LocationManager.NETWORK_PROVIDER));
        } catch (Exception ignored) { }
    }

    private void configureLocation() {
        if (locationManager == null || !hasLocationPermission()) return;
        stopLocation();
        int minutes = shortestActiveInterval();
        if (minutes <= 0) return;
        long interval = minutes * 60000L;
        if (phoneBattery < 20) interval *= 4L;
        try {
            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
												   interval, 10f, this);
            locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
												   interval, 10f, this);
        } catch (Exception ignored) { }
    }

    private int shortestActiveInterval() {
        List<DeviceRecord> rows = db.tracked();
        int shortest = Integer.MAX_VALUE;
        for (int i = 0; i < rows.size(); i++) {
            DeviceRecord record = rows.get(i);
            if (isLocationEligible(record, System.currentTimeMillis()) && record.trackingPriority > 0 &&
                record.trackingPriority < shortest) shortest = record.trackingPriority;
        }
        return shortest == Integer.MAX_VALUE ? 0 : shortest;
    }

    private void stopLocation() {
        try { if (locationManager != null) locationManager.removeUpdates(this); }
        catch (Exception ignored) { }
    }

    private void captureFresh(final String address, final long eventTime) {
        if (locationManager == null || !hasLocationPermission()) return;
        final LocationListener once = new LocationListener() {
            private boolean complete;
            public void onLocationChanged(Location location) {
                if (complete || location == null) return;
                complete = true;
                lastLocation = location;
                db.updateLocation(address, fix(location), eventTime);
                notifyChanged();
                try { locationManager.removeUpdates(this); } catch (Exception ignored) { }
            }
            public void onProviderDisabled(String provider) { }
            public void onProviderEnabled(String provider) { }
            public void onStatusChanged(String provider, int status, Bundle extras) { }
        };
        try {
            locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, once, null);
            locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, once, null);
            handler.postDelayed(new Runnable() {
					public void run() {
						try { locationManager.removeUpdates(once); } catch (Exception ignored) { }
					}
				}, 15000L);
        } catch (Exception ignored) { }
    }

    public void onLocationChanged(Location location) {
        if (location == null) return;
        lastLocation = location;
        TrackerDatabase.Fix fix = fix(location);
        long now = System.currentTimeMillis();
        List<DeviceRecord> rows = db.tracked();
        for (int i = 0; i < rows.size(); i++) {
            DeviceRecord record = rows.get(i);
            if (!isLocationEligible(record, now) || record.trackingPriority <= 0) continue;
            long interval = record.trackingPriority * 60000L;
            if (phoneBattery < 20) interval *= 4L;
            Long previous = lastPeriodicLog.get(record.address);
            if (previous == null || now - previous.longValue() >= interval) {
                db.updateLocation(record.address, fix, now);
                lastPeriodicLog.put(record.address, Long.valueOf(now));
            }
        }
        notifyChanged();
    }

    public void onProviderDisabled(String provider) { }
    public void onProviderEnabled(String provider) { }
    public void onStatusChanged(String provider, int status, Bundle extras) { }

    private TrackerDatabase.Fix currentFix() {
        return lastLocation == null ? null : fix(lastLocation);
    }

    private TrackerDatabase.Fix fix(Location location) {
        return new TrackerDatabase.Fix(location.getLatitude(), location.getLongitude(),
									   location.hasAccuracy() ? location.getAccuracy() : 0);
    }
    private void resolveVendor(String address, int companyId) {
        String vendor = companyId >= 0 ? db.lookupCompany(companyId) : null;
        if (vendor == null) vendor = db.lookupOui(address);
        db.setVendor(address, vendor);
    }

    private String bytesToHex(byte[] data) {
        if (data == null) return null;
        StringBuilder value = new StringBuilder(data.length * 2);
        final char[] digits = "0123456789ABCDEF".toCharArray();
        for (int i = 0; i < data.length; i++) {
            int item = data[i] & 0xff;
            value.append(digits[item >>> 4]); value.append(digits[item & 0x0f]);
        }
        return value.toString();
    }

    private int parseCompanyId(byte[] scanRecord) {
        if (scanRecord == null) return -1;
        int index = 0;
        while (index < scanRecord.length) {
            int length = scanRecord[index] & 0xff;
            if (length == 0 || index + length >= scanRecord.length) break;
            int type = scanRecord[index + 1] & 0xff;
            if (type == 0xff && length >= 3)
                return (scanRecord[index + 2] & 0xff) |
                    ((scanRecord[index + 3] & 0xff) << 8);
            index += length + 1;
        }
        return -1;
    }

    private String infer(DeviceRecord record) {
        if (record == null) return "Manual / Intentional Disconnect";
        if (record.battery >= 0 && record.battery < 10)
            return "Likely Dead Battery / Power Loss";
        if (record.rssi <= -80 && record.rssi >= -100) return "Walked Out of Range";
        if (record.rssi >= -65 && record.rssi <= -30)
            return "Likely Dead Battery / Power Loss";
        return "Manual / Intentional Disconnect";
    }

    private void disconnectAll(String reason) {
        long now = System.currentTimeMillis();
        List<DeviceRecord> rows = db.tracked();
        for (int i = 0; i < rows.size(); i++) {
            DeviceRecord record = rows.get(i);
            if (record.connected == 1) {
                db.disconnected(record.address, record.broadcastName, now, currentFix(), reason);
                captureFresh(record.address, now);
            }
        }
        notifyChanged();
    }

    private void syncPairedDevices() {
        if (bluetooth == null) return;
        try {
            for (BluetoothDevice device : bluetooth.getBondedDevices()) {
                String value = address(device);
                if (value != null) {
                    db.markTracked(value, name(device));
                    resolveVendor(value, -1);
                }
            }
        } catch (Exception ignored) { }
    }

    private void restartBackgroundScan() {
        handler.removeCallbacks(scanTask);
        handler.post(scanTask);
    }

    private boolean shouldBackgroundScan() {
        List<DeviceRecord> rows = db.tracked();
        for (int i = 0; i < rows.size(); i++)
            if (rows.get(i).connected == 1 || rows.get(i).trackBroadcast == 1) return true;
        return false;
    }

    private boolean isLocationEligible(DeviceRecord record, long now) {
        return record.connected == 1 || (record.trackBroadcast == 1 && record.lastRssiAt > 0 &&
            now - record.lastRssiAt <= 180000L);
    }

    private void notifyChanged() {
        sendBroadcast(new Intent(ACTION_DATA_CHANGED).setPackage(getPackageName()));
    }

    private boolean hasLocationPermission() {
        return getPackageManager().checkPermission("android.permission.ACCESS_FINE_LOCATION",
												   getPackageName()) == PackageManager.PERMISSION_GRANTED ||
            getPackageManager().checkPermission("android.permission.ACCESS_COARSE_LOCATION",
												getPackageName()) == PackageManager.PERMISSION_GRANTED;
    }

    private boolean hasBluetoothPermission() {
        return Build.VERSION.SDK_INT < 31 || getPackageManager().checkPermission(
            "android.permission.BLUETOOTH_SCAN", getPackageName()) ==
            PackageManager.PERMISSION_GRANTED;
    }

    private String address(BluetoothDevice device) {
        try { return device.getAddress(); } catch (Exception error) { return null; }
    }

    private String name(BluetoothDevice device) {
        try {
            String value = device.getName();
            return value == null ? "Unknown item" : value;
        } catch (Exception error) { return "Unknown item"; }
    }

    private Location newer(Location first, Location second) {
        if (first == null) return second;
        if (second == null) return first;
        return first.getTime() >= second.getTime() ? first : second;
    }

    /**
     * FLAG_IMMUTABLE (API 23+) is referenced by value because this module
     * still compiles against SDK 21.
     */
    private int pendingFlags() {
        int flags = PendingIntent.FLAG_UPDATE_CURRENT;
        if (Build.VERSION.SDK_INT >= 23) flags |= 0x04000000;
        return flags;
    }

    private int alertId(String address) {
        return 1000 + Math.abs(address.hashCode() % 100000);
    }

    private PendingIntent acknowledgeIntent(String address) {
        Intent ack = new Intent(ACTION_ACK_LEFT_BEHIND).setPackage(getPackageName());
        if (address != null) ack.putExtra(EXTRA_ADDRESS, address);
        return PendingIntent.getBroadcast(this,
            address == null ? 0 : alertId(address), ack, pendingFlags());
    }

    /**
     * Records the item as left behind and keeps alerting until the user
     * taps "Acknowledge Left Behind" - a swipe alone does not clear it.
     */
    private void markLeftBehind(DeviceRecord record, String reason) {
        if (record == null || record.address == null) return;
        leftBehind.put(record.address, record.displayName());
        showDisconnectAlert(record, reason);
        updateForegroundNotification();
        startAlertLoop();
    }

    private void acknowledgeLeftBehind(String address) {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        if (address == null) {
            for (String pending : leftBehind.keySet().toArray(new String[0])) {
                try { manager.cancel(alertId(pending)); } catch (Exception ignored) { }
            }
            leftBehind.clear();
        } else {
            leftBehind.remove(address);
            try { manager.cancel(alertId(address)); } catch (Exception ignored) { }
        }
        if (leftBehind.isEmpty()) {
            handler.removeCallbacks(alertTask);
            alertLoopRunning = false;
        }
        updateForegroundNotification();
    }

    private void startAlertLoop() {
        if (alertLoopRunning) return;
        alertLoopRunning = true;
        handler.postDelayed(alertTask, ALERT_REPEAT_MS);
    }

    /** Re-fires every outstanding alert until each one is acknowledged. */
    private final Runnable alertTask = new Runnable() {
        public void run() {
            if (leftBehind.isEmpty()) { alertLoopRunning = false; return; }
            for (String address : leftBehind.keySet().toArray(new String[0])) {
                DeviceRecord record = db.get(address);
                if (record == null) continue;
                showDisconnectAlert(record, "Still left behind - not acknowledged");
            }
            updateForegroundNotification();
            handler.postDelayed(this, ALERT_REPEAT_MS);
        }
    };

    private void updateForegroundNotification() {
        try {
            NotificationManager manager =
                (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            manager.notify(NOTIFICATION_ID, buildNotification());
        } catch (Exception ignored) { }
    }

    private void showDisconnectAlert(DeviceRecord record, String reason) {
        Intent open = new Intent(this, MainActivity.class);
        PendingIntent pending = PendingIntent.getActivity(this, record.address.hashCode(), open,
														  PendingIntent.FLAG_UPDATE_CURRENT);
        Notification.Builder builder = new Notification.Builder(this)
            .setSmallIcon(R.drawable.ic_launcher)
            .setContentTitle(record.displayName() + " left behind")
            .setContentText(reason == null ? "Your tracked item is no longer connected" : reason)
            .setContentIntent(pending).setAutoCancel(true)
            .setDefaults(Notification.DEFAULT_ALL).setPriority(Notification.PRIORITY_HIGH);
        if (Build.VERSION.SDK_INT >= 16) {
            builder.addAction(0, "Acknowledge Left Behind",
                              acknowledgeIntent(record.address));
        }
        NotificationManager manager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        if (Build.VERSION.SDK_INT >= 26) {
            try {
                Class<?> channelClass = Class.forName("android.app.NotificationChannel");
                Object channel = channelClass.getConstructor(String.class, CharSequence.class, Integer.TYPE)
                    .newInstance(ALERT_CHANNEL_ID, "Tracked item disconnects", 4);
                channelClass.getMethod("enableVibration", Boolean.TYPE).invoke(channel, Boolean.TRUE);
                NotificationManager.class.getMethod("createNotificationChannel", channelClass).invoke(manager, channel);
                Notification.Builder.class.getMethod("setChannelId", String.class).invoke(builder, ALERT_CHANNEL_ID);
            } catch (Exception ignored) { }
        }
        try { manager.notify(1000 + Math.abs(record.address.hashCode() % 100000), builder.build()); }
        catch (Exception ignored) { }
    }

    private Notification buildNotification() {
        Intent open = new Intent(this, MainActivity.class);
        PendingIntent pending = PendingIntent.getActivity(this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT);
        int outstanding = leftBehind.size();
        String title = outstanding > 0
            ? outstanding + (outstanding == 1 ? " item left behind" : " items left behind")
            : "Bluetooth Item Finder";
        String text = outstanding > 0
            ? "Alerting until acknowledged: " + describeLeftBehind()
            : "Watching your tracked items";
        Notification.Builder builder = new Notification.Builder(this)
            .setSmallIcon(R.drawable.ic_launcher).setContentTitle(title)
            .setContentText(text).setOngoing(true)
            .setContentIntent(pending);
        if (Build.VERSION.SDK_INT >= 16 && outstanding > 0) {
            builder.addAction(0,
                              outstanding == 1 ? "Acknowledge Left Behind" : "Acknowledge All",
                              acknowledgeIntent(outstanding == 1
                                                ? leftBehind.keySet().iterator().next() : null));
        }
        if (Build.VERSION.SDK_INT >= 26) {
            try {
                Class<?> channelClass = Class.forName("android.app.NotificationChannel");
                Object channel = channelClass.getConstructor(String.class,
															 CharSequence.class, Integer.TYPE).newInstance(CHANNEL_ID,
																										   "Item tracking", 2);
                NotificationManager manager = (NotificationManager)
                    getSystemService(NOTIFICATION_SERVICE);
                NotificationManager.class.getMethod("createNotificationChannel", channelClass)
                    .invoke(manager, channel);
                Notification.Builder.class.getMethod("setChannelId", String.class)
                    .invoke(builder, CHANNEL_ID);
            } catch (Exception ignored) { }
        }
        return builder.build();
    }

    private String describeLeftBehind() {
        StringBuilder names = new StringBuilder();
        for (String name : leftBehind.values()) {
            if (names.length() > 0) names.append(", ");
            names.append(name);
        }
        return names.toString();
    }
}

