package org.whoneedhelp.mobile;

import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.graphics.drawable.Icon;
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 android.os.Looper;
import android.webkit.CookieManager;

import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

public final class TrackingService extends Service implements LocationListener {
    private static final String ACTION_START =
        "org.whoneedhelp.mobile.action.START_TRACKING";
    private static final String ACTION_STOP_REMOTE =
        "org.whoneedhelp.mobile.action.STOP_TRACKING_REMOTE";
    private static final String EXTRA_ASSIGNMENT_ID = "assignment_id";
    private static final String EXTRA_CSRF_TOKEN = "csrf_token";
    private static final String EXTRA_COOKIE = "cookie";
    private static final String CHANNEL_ID = "active_help_tracking";
    private static final int NOTIFICATION_ID = 4101;

    private final Object pendingLocationLock = new Object();
    private final AtomicBoolean uploadRunning = new AtomicBoolean(false);
    private final AtomicBoolean stopRequestRunning = new AtomicBoolean(false);
    private final Handler mainHandler = new Handler(Looper.getMainLooper());

    private ExecutorService networkExecutor;
    private LocationManager locationManager;
    private Location pendingLocation;
    private String assignmentId;
    private String csrfToken;
    private String cookie;

    public static Intent startIntent(
        android.content.Context context,
        String assignmentId,
        String csrfToken,
        String cookie
    ) {
        return new Intent(context, TrackingService.class)
            .setAction(ACTION_START)
            .putExtra(EXTRA_ASSIGNMENT_ID, assignmentId)
            .putExtra(EXTRA_CSRF_TOKEN, csrfToken)
            .putExtra(EXTRA_COOKIE, cookie);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        networkExecutor = Executors.newSingleThreadExecutor();
        locationManager = getSystemService(LocationManager.class);
        createNotificationChannel();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent == null ? null : intent.getAction();

        if (ACTION_STOP_REMOTE.equals(action)) {
            if (
                !validCredential(assignmentId)
                    || !validCredential(csrfToken)
                    || !validCredential(cookie)
            ) {
                stopTrackingLocally();
                return START_NOT_STICKY;
            }

            pauseLocationUpdates();
            showNotification(
                getString(R.string.tracking_stopping),
                getString(R.string.tracking_stopping_detail)
            );
            requestRemoteStop();
            return START_NOT_STICKY;
        }

        if (!ACTION_START.equals(action)) {
            stopSelf();
            return START_NOT_STICKY;
        }

        assignmentId = intent.getStringExtra(EXTRA_ASSIGNMENT_ID);
        csrfToken = intent.getStringExtra(EXTRA_CSRF_TOKEN);
        cookie = intent.getStringExtra(EXTRA_COOKIE);

        if (!validCredential(assignmentId) || !validCredential(csrfToken) || !validCredential(cookie)) {
            stopSelf();
            return START_NOT_STICKY;
        }

        try {
            promoteToForeground();
            requestLocationUpdates();
        } catch (SecurityException exception) {
            stopTrackingLocally();
        }

        return START_NOT_STICKY;
    }

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

    @Override
    public void onLocationChanged(Location location) {
        synchronized (pendingLocationLock) {
            pendingLocation = new Location(location);
        }

        dispatchPendingLocation();
    }

    @Override
    public void onProviderEnabled(String provider) {
        // The active provider will deliver a new point without a route history.
    }

    @Override
    public void onProviderDisabled(String provider) {
        // Other enabled providers, if any, remain registered.
    }

    @SuppressWarnings("deprecation")
    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        // Required by LocationListener on supported older Android releases.
    }

    @Override
    public void onDestroy() {
        pauseLocationUpdates();
        networkExecutor.shutdownNow();
        super.onDestroy();
    }

    private void promoteToForeground() {
        Notification notification = trackingNotification(
            getString(R.string.tracking_active),
            getString(R.string.tracking_active_detail)
        );

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(
                NOTIFICATION_ID,
                notification,
                ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
            );
        } else {
            startForeground(NOTIFICATION_ID, notification);
        }
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }

        NotificationChannel channel = new NotificationChannel(
            CHANNEL_ID,
            getString(R.string.tracking_channel_name),
            NotificationManager.IMPORTANCE_LOW
        );
        channel.setDescription(getString(R.string.tracking_channel_description));
        getSystemService(NotificationManager.class).createNotificationChannel(channel);
    }

    @SuppressWarnings("deprecation")
    private Notification trackingNotification(String title, String text) {
        Intent openIntent = new Intent(this, MainActivity.class)
            .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
        PendingIntent openPendingIntent = PendingIntent.getActivity(
            this,
            0,
            openIntent,
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );

        Intent stopIntent = new Intent(this, TrackingService.class)
            .setAction(ACTION_STOP_REMOTE);
        PendingIntent stopPendingIntent = PendingIntent.getService(
            this,
            1,
            stopIntent,
            PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
        );
        Notification.Action stopAction = new Notification.Action.Builder(
            Icon.createWithResource(this, R.drawable.ic_notification),
            getString(R.string.tracking_stop_action),
            stopPendingIntent
        ).build();

        Notification.Builder builder =
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
                ? new Notification.Builder(this, CHANNEL_ID)
                : new Notification.Builder(this);

        return builder
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(text)
            .setContentIntent(openPendingIntent)
            .setCategory(Notification.CATEGORY_SERVICE)
            .setOngoing(true)
            .setOnlyAlertOnce(true)
            .addAction(stopAction)
            .build();
    }

    private void showNotification(String title, String text) {
        getSystemService(NotificationManager.class)
            .notify(NOTIFICATION_ID, trackingNotification(title, text));
    }

    private void requestLocationUpdates() {
        boolean locationGranted =
            checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED
                || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
                == PackageManager.PERMISSION_GRANTED;

        if (!locationGranted) {
            throw new SecurityException("Location permission is unavailable.");
        }

        boolean registered = false;
        registered |= requestProviderIfEnabled(LocationManager.GPS_PROVIDER);
        registered |= requestProviderIfEnabled(LocationManager.NETWORK_PROVIDER);

        if (!registered) {
            showNotification(
                getString(R.string.tracking_location_unavailable),
                getString(R.string.tracking_location_unavailable_detail)
            );
            return;
        }

    }

    @SuppressWarnings("MissingPermission")
    private boolean requestProviderIfEnabled(String provider) {
        if (!locationManager.isProviderEnabled(provider)) {
            return false;
        }

        locationManager.requestLocationUpdates(
            provider,
            BuildConfig.TRACKING_MIN_TIME_MS,
            0.0f,
            this,
            Looper.getMainLooper()
        );
        return true;
    }

    private void pauseLocationUpdates() {
        if (locationManager != null) {
            try {
                locationManager.removeUpdates(this);
            } catch (SecurityException ignored) {
                // Permissions may have been revoked while sharing was active.
            }
        }

        synchronized (pendingLocationLock) {
            pendingLocation = null;
        }
    }

    private void dispatchPendingLocation() {
        if (!uploadRunning.compareAndSet(false, true)) {
            return;
        }

        networkExecutor.execute(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    Location location;

                    synchronized (pendingLocationLock) {
                        location = pendingLocation;
                        pendingLocation = null;
                    }

                    if (location == null) {
                        return;
                    }

                    int status = sendPosition(location);

                    if (status == HttpURLConnection.HTTP_UNAUTHORIZED
                        || status == HttpURLConnection.HTTP_FORBIDDEN
                        || status == HttpURLConnection.HTTP_NOT_FOUND
                        || status == HttpURLConnection.HTTP_CONFLICT) {
                        mainHandler.post(this::stopTrackingLocally);
                        return;
                    }
                }
            } finally {
                uploadRunning.set(false);

                synchronized (pendingLocationLock) {
                    if (pendingLocation != null) {
                        dispatchPendingLocation();
                    }
                }
            }
        });
    }

    private int sendPosition(Location location) {
        String body = String.format(
            Locale.US,
            "{\"latitude\":%.8f,\"longitude\":%.8f,\"accuracy_meters\":%.2f}",
            location.getLatitude(),
            location.getLongitude(),
            location.getAccuracy()
        );

        return post(
            "/mobile/tracking/" + assignmentId + "/position",
            body.getBytes(StandardCharsets.UTF_8)
        );
    }

    private void requestRemoteStop() {
        if (!stopRequestRunning.compareAndSet(false, true)) {
            return;
        }

        networkExecutor.execute(() -> {
            try {
                int status = post(
                    "/mobile/tracking/" + assignmentId + "/stop",
                    "{}".getBytes(StandardCharsets.UTF_8)
                );

                if (status == HttpURLConnection.HTTP_NO_CONTENT
                    || status == HttpURLConnection.HTTP_NOT_FOUND
                    || status == HttpURLConnection.HTTP_CONFLICT) {
                    mainHandler.post(this::stopTrackingLocally);
                } else {
                    mainHandler.post(() ->
                        showNotification(
                            getString(R.string.tracking_stop_failed),
                            getString(R.string.tracking_stop_failed_detail)
                        )
                    );
                }
            } finally {
                stopRequestRunning.set(false);
            }
        });
    }

    private int post(String path, byte[] body) {
        HttpURLConnection connection = null;

        try {
            URL url = new URI(BuildConfig.BASE_URL).resolve(path).toURL();
            connection = (HttpURLConnection) url.openConnection();
            connection.setInstanceFollowRedirects(false);
            connection.setRequestMethod("POST");
            connection.setConnectTimeout((int) BuildConfig.TRACKING_HTTP_TIMEOUT_MS);
            connection.setReadTimeout((int) BuildConfig.TRACKING_HTTP_TIMEOUT_MS);
            connection.setRequestProperty("Accept", "application/json");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("X-CSRF-Token", csrfToken);
            connection.setRequestProperty("Cookie", cookie);
            connection.setDoOutput(true);
            connection.setFixedLengthStreamingMode(body.length);

            try (OutputStream output = connection.getOutputStream()) {
                output.write(body);
            }

            int status = connection.getResponseCode();

            for (java.util.Map.Entry<String, java.util.List<String>> header
                : connection.getHeaderFields().entrySet()) {
                if (header.getKey() != null && header.getKey().equalsIgnoreCase("Set-Cookie")) {
                    for (String setCookie : header.getValue()) {
                        mainHandler.post(() ->
                            CookieManager.getInstance().setCookie(BuildConfig.BASE_URL, setCookie)
                        );
                    }
                }
            }

            return status;
        } catch (Exception exception) {
            return -1;
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    private void stopTrackingLocally() {
        pauseLocationUpdates();
        stopForeground(STOP_FOREGROUND_REMOVE);
        stopSelf();
    }

    private static boolean validCredential(String value) {
        return value != null
            && !value.trim().isEmpty()
            && value.indexOf('\r') < 0
            && value.indexOf('\n') < 0;
    }
}
