package org.whoneedhelp.mobile;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.GeolocationPermissions;
import android.webkit.SslErrorHandler;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import androidx.activity.ComponentActivity;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.webkit.WebMessageCompat;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewFeature;

import java.util.ArrayList;
import java.util.Collections;
import java.util.UUID;

import org.json.JSONException;
import org.json.JSONObject;

public final class MainActivity extends ComponentActivity {
    private static final String LOG_TAG = "WhoNeedHelpWebView";
    private WebView webView;
    private TrustedOrigin trustedOrigin;
    private GeolocationPermissions.Callback pendingLocationCallback;
    private String pendingLocationOrigin;
    private ActivityResultLauncher<String[]> locationPermissionLauncher;
    private ActivityResultLauncher<String[]> nativeTrackingPermissionLauncher;
    private PendingNativeTracking pendingNativeTracking;
    private AlertDialog pageLoadErrorDialog;
    private boolean mainFrameLoadFailed;

    @Override
    @SuppressLint("SetJavaScriptEnabled")
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        trustedOrigin = TrustedOrigin.parse(BuildConfig.BASE_URL, BuildConfig.DEBUG);
        locationPermissionLauncher = registerForActivityResult(
            new ActivityResultContracts.RequestMultiplePermissions(),
            result -> {
                boolean granted =
                    Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))
                        || Boolean.TRUE.equals(
                            result.get(Manifest.permission.ACCESS_COARSE_LOCATION)
                        );
                completeLocationPermission(granted);
            }
        );
        nativeTrackingPermissionLauncher = registerForActivityResult(
            new ActivityResultContracts.RequestMultiplePermissions(),
            result -> {
                boolean locationGranted =
                    Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))
                        || Boolean.TRUE.equals(
                            result.get(Manifest.permission.ACCESS_COARSE_LOCATION)
                        )
                        || hasLocationPermission();

                if (locationGranted) {
                    startPendingNativeTracking();
                } else {
                    pendingNativeTracking = null;
                    dispatchNativeTrackingError();
                }
            }
        );
        webView = new WebView(this);
        webView.setLayoutParams(
            new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
        );
        setContentView(webView);

        WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG);

        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setDomStorageEnabled(true);
        settings.setGeolocationEnabled(true);
        settings.setAllowFileAccess(false);
        settings.setAllowContentAccess(false);
        settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
        settings.setMediaPlaybackRequiresUserGesture(true);
        settings.setUserAgentString(settings.getUserAgentString() + " WhoNeedHelpAndroid/0.1");

        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(true);
        cookieManager.setAcceptThirdPartyCookies(webView, false);

        configureNativeTrackingBridge();
        webView.setWebViewClient(new TrustedWebViewClient());
        webView.setWebChromeClient(new LocationWebChromeClient());
        getOnBackPressedDispatcher().addCallback(
            this,
            new OnBackPressedCallback(true) {
                @Override
                public void handleOnBackPressed() {
                    if (webView.canGoBack()) {
                        webView.goBack();
                        return;
                    }

                    setEnabled(false);
                    getOnBackPressedDispatcher().onBackPressed();
                }
            }
        );

        if (savedInstanceState == null || webView.restoreState(savedInstanceState) == null) {
            webView.loadUrl(initialUrl());
        }
    }

    private String initialUrl() {
        return LaunchUrlResolver.initialUrl(
            trustedOrigin,
            getIntent().getDataString(),
            getIntent().getStringExtra("initial_url"),
            BuildConfig.DEBUG
        );
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);

        String candidate = LaunchUrlResolver.incomingUrl(
            trustedOrigin,
            intent.getDataString(),
            intent.getStringExtra("initial_url"),
            BuildConfig.DEBUG
        );

        if (candidate != null && webView != null) {
            webView.loadUrl(candidate);
        }
    }

    private static String safeLogPath(Uri uri) {
        String path = uri.getPath();

        if (path != null && path.startsWith("/users/log-in/")) {
            return "/users/log-in/[redacted]";
        }

        return path;
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        webView.saveState(outState);
        super.onSaveInstanceState(outState);
    }

    @Override
    protected void onPause() {
        webView.onPause();
        CookieManager.getInstance().flush();
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        webView.onResume();
    }

    @Override
    protected void onDestroy() {
        denyPendingLocation();
        dismissPageLoadError();

        if (webView != null) {
            webView.stopLoading();
            webView.setWebChromeClient(null);
            webView.setWebViewClient(null);
            webView.destroy();
        }

        super.onDestroy();
    }

    private void requestLocationFor(
        String origin,
        GeolocationPermissions.Callback callback
    ) {
        if (!trustedOrigin.matchesOrigin(origin)) {
            callback.invoke(origin, false, false);
            return;
        }

        denyPendingLocation();
        pendingLocationOrigin = origin;
        pendingLocationCallback = callback;

        boolean granted =
            checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED
                || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
                == PackageManager.PERMISSION_GRANTED;

        if (granted) {
            completeLocationPermission(true);
            return;
        }

        locationPermissionLauncher.launch(
            new String[] {
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION
            }
        );
    }

    private boolean hasLocationPermission() {
        return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED
            || checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)
                == PackageManager.PERMISSION_GRANTED;
    }

    private void prepareNativeTracking(String assignmentId, String csrfToken) {
        if (
            !validAssignmentId(assignmentId)
                || csrfToken == null
                || csrfToken.trim().isEmpty()
        ) {
            dispatchNativeTrackingError();
            return;
        }

        pendingNativeTracking = new PendingNativeTracking(assignmentId, csrfToken);

        boolean notificationGranted =
            Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
                || checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
                    == PackageManager.PERMISSION_GRANTED;

        if (hasLocationPermission() && notificationGranted) {
            startPendingNativeTracking();
            return;
        }

        ArrayList<String> permissions = new ArrayList<>();
        if (!hasLocationPermission()) {
            permissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
            permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !notificationGranted) {
            permissions.add(Manifest.permission.POST_NOTIFICATIONS);
        }

        nativeTrackingPermissionLauncher.launch(permissions.toArray(new String[0]));
    }

    private void configureNativeTrackingBridge() {
        if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
            return;
        }

        WebViewCompat.addWebMessageListener(
            webView,
            "WhoNeedHelpAndroid",
            Collections.singleton(trustedOrigin.originRule()),
            (
                WebView view,
                WebMessageCompat message,
                Uri sourceOrigin,
                boolean isMainFrame,
                androidx.webkit.JavaScriptReplyProxy replyProxy
            ) -> {
                if (
                    !isMainFrame
                        || sourceOrigin == null
                        || !trustedOrigin.matchesOrigin(sourceOrigin.toString())
                ) {
                    return;
                }

                handleNativeTrackingMessage(message.getData());
            }
        );
    }

    private void handleNativeTrackingMessage(String payload) {
        if (payload == null) {
            dispatchNativeTrackingError();
            return;
        }

        try {
            JSONObject message = new JSONObject(payload);
            String action = message.optString("action", "");

            if ("start".equals(action)) {
                prepareNativeTracking(
                    message.optString("assignment_id", ""),
                    message.optString("csrf_token", "")
                );
            } else if ("stop".equals(action)) {
                stopService(new Intent(MainActivity.this, TrackingService.class));
            } else {
                dispatchNativeTrackingError();
            }
        } catch (JSONException exception) {
            dispatchNativeTrackingError();
        }
    }

    private void startPendingNativeTracking() {
        PendingNativeTracking pending = pendingNativeTracking;
        pendingNativeTracking = null;

        if (pending == null) {
            return;
        }

        String cookie = CookieManager.getInstance().getCookie(BuildConfig.BASE_URL);

        if (cookie == null || cookie.trim().isEmpty()) {
            dispatchNativeTrackingError();
            return;
        }

        Intent intent = TrackingService.startIntent(
            this,
            pending.assignmentId,
            pending.csrfToken,
            cookie
        );

        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(intent);
            } else {
                startService(intent);
            }
        } catch (RuntimeException exception) {
            dispatchNativeTrackingError();
        }
    }

    private void dispatchNativeTrackingError() {
        if (webView == null) {
            return;
        }

        webView.evaluateJavascript(
            "window.dispatchEvent(new CustomEvent('wnh:native-tracking-error'))",
            null
        );
    }

    private static boolean validAssignmentId(String value) {
        try {
            UUID.fromString(value);
            return true;
        } catch (IllegalArgumentException | NullPointerException exception) {
            return false;
        }
    }

    private void completeLocationPermission(boolean granted) {
        if (pendingLocationCallback != null && pendingLocationOrigin != null) {
            pendingLocationCallback.invoke(pendingLocationOrigin, granted, false);
        }

        pendingLocationCallback = null;
        pendingLocationOrigin = null;
    }

    private void denyPendingLocation() {
        completeLocationPermission(false);
    }

    private void openExternal(Uri uri) {
        String scheme = uri.getScheme();

        if (
            scheme == null ||
            !(
                scheme.equalsIgnoreCase("http")
                    || scheme.equalsIgnoreCase("https")
                    || scheme.equalsIgnoreCase("mailto")
                    || scheme.equalsIgnoreCase("tel")
            )
        ) {
            Toast.makeText(this, R.string.unsupported_link, Toast.LENGTH_SHORT).show();
            return;
        }

        try {
            startActivity(new Intent(Intent.ACTION_VIEW, uri));
        } catch (ActivityNotFoundException exception) {
            Toast.makeText(this, R.string.no_link_handler, Toast.LENGTH_SHORT).show();
        }
    }

    private void showPageLoadError(int messageResource) {
        if (isFinishing() || isDestroyed()) {
            return;
        }

        dismissPageLoadError();
        pageLoadErrorDialog = new AlertDialog.Builder(this)
            .setTitle(R.string.page_load_failed_title)
            .setMessage(messageResource)
            .setPositiveButton(R.string.retry, (dialog, which) -> {
                if (webView != null) {
                    webView.reload();
                }
            })
            .setNegativeButton(R.string.close, (dialog, which) -> finish())
            .create();
        pageLoadErrorDialog.setOnDismissListener(dialog -> pageLoadErrorDialog = null);
        pageLoadErrorDialog.show();
    }

    private void dismissPageLoadError() {
        AlertDialog dialog = pageLoadErrorDialog;
        pageLoadErrorDialog = null;

        if (dialog != null && dialog.isShowing()) {
            dialog.setOnDismissListener(null);
            dialog.dismiss();
        }
    }

    private final class TrustedWebViewClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
            Uri uri = request.getUrl();

            if (trustedOrigin.matches(uri.toString())) {
                return false;
            }

            openExternal(uri);
            return true;
        }

        @Override
        public void onReceivedSslError(
            WebView view,
            SslErrorHandler handler,
            SslError error
        ) {
            mainFrameLoadFailed = true;
            handler.cancel();
            showPageLoadError(R.string.secure_connection_failed);
        }

        @Override
        public void onReceivedError(
            WebView view,
            WebResourceRequest request,
            WebResourceError error
        ) {
            if (request.isForMainFrame()) {
                mainFrameLoadFailed = true;

                if (BuildConfig.DEBUG) {
                    Log.e(
                        LOG_TAG,
                        "Main-frame load failed: code="
                            + error.getErrorCode()
                            + " path="
                            + safeLogPath(request.getUrl())
                    );
                }

                showPageLoadError(R.string.page_load_failed);
            }
        }

        @Override
        public void onReceivedHttpError(
            WebView view,
            WebResourceRequest request,
            WebResourceResponse errorResponse
        ) {
            if (request.isForMainFrame()) {
                mainFrameLoadFailed = true;
                showPageLoadError(R.string.page_load_failed);
            }
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            mainFrameLoadFailed = false;
            dismissPageLoadError();

            if (BuildConfig.DEBUG) {
                Log.d(LOG_TAG, "Main-frame load started: path=" + safeLogPath(Uri.parse(url)));
            }

            if (!trustedOrigin.matches(url)) {
                view.stopLoading();
                openExternal(Uri.parse(url));
            }
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            if (!mainFrameLoadFailed) {
                dismissPageLoadError();
            }

            if (BuildConfig.DEBUG) {
                Log.d(LOG_TAG, "Main-frame load finished: path=" + safeLogPath(Uri.parse(url)));
                scheduleMapDiagnostics(view, Uri.parse(url));
            }
        }

        private void scheduleMapDiagnostics(WebView view, Uri uri) {
            String path = uri.getPath();

            if (path == null || !path.startsWith("/requests/")) {
                return;
            }

            view.postDelayed(
                () ->
                    view.evaluateJavascript(
                        "(() => {"
                            + "const map=document.getElementById('show-map');"
                            + "if(!map)return 'map=missing';"
                            + "return 'map=present;canvas='+map.querySelectorAll('canvas').length"
                            + "+';controls='+map.querySelectorAll('.maplibregl-ctrl').length"
                            + "+';size='+map.clientWidth+'x'+map.clientHeight;"
                            + "})()",
                        result -> Log.d(LOG_TAG, "Map diagnostics: " + result)
                    ),
                4_000
            );
        }
    }

    private final class LocationWebChromeClient extends WebChromeClient {
        @Override
        public void onGeolocationPermissionsShowPrompt(
            String origin,
            GeolocationPermissions.Callback callback
        ) {
            requestLocationFor(origin, callback);
        }

        @Override
        public void onGeolocationPermissionsHidePrompt() {
            denyPendingLocation();
        }
    }

    private static final class PendingNativeTracking {
        private final String assignmentId;
        private final String csrfToken;

        private PendingNativeTracking(String assignmentId, String csrfToken) {
            this.assignmentId = assignmentId;
            this.csrfToken = csrfToken;
        }
    }
}
