package com.appzung.codepush.react;

import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;

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

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Native fire-and-forget reporter for {@code /report_status/deploy} failures.
 * <p>
 * Built so failure detail can be POSTed the <b>moment</b> it occurs in native
 * code, without waiting for the React Native bridge to come up. Intentionally
 * mirrors the wire format produced by the JS-side
 * {@code CodePushApiSdk.reportStatusDeploy} so the server sees identical
 * payloads regardless of which path produced them.
 * <p>
 * Uses the JDK-built-in {@link HttpURLConnection} (no external HTTP client
 * dependency) and runs all I/O on a dedicated single-thread executor —
 * keeps ordering FIFO and avoids stepping on the caller thread.
 * <p>
 * Network errors are silently swallowed; the existing
 * {@code saveFailedUpdate(__codePushFailureInfo) + sNeedToReportRollback=true}
 * persistence still runs alongside this, providing a next-sync fallback.
 * <p>
 * Internal — not part of the public CodePush API surface.
 */
public final class CodePushDeployReporter {

    private static final String TAG = "[DeployReporter] ";

    private static final String PATH_REPORT_DEPLOY = "/report_status/deploy";
    private static final String STATUS_DEPLOYMENT_FAILED = "DeploymentFailed";

    // Wire keys (snake_case) — must match CodePushApiSdk.reportStatusDeploy.
    private static final String KEY_DEPLOYMENT_KEY = "deployment_key";
    private static final String KEY_APP_VERSION = "app_version";
    private static final String KEY_CLIENT_UNIQUE_ID = "client_unique_id";
    private static final String KEY_STATUS = "status";
    private static final String KEY_LABEL = "label";
    private static final String KEY_ERROR_CODE = "error_code";
    private static final String KEY_ERROR_MESSAGE = "error_message";
    private static final String KEY_FAILED_PHASE = "failed_phase";
    private static final String KEY_BINARY_APP_VERSION = "binary_app_version";

    // Package-side keys (mirrors CodePushUpdateManager.getCurrentPackage()).
    private static final String PKG_KEY_LABEL = "label";
    private static final String PKG_KEY_APP_VERSION = "appVersion";

    // Wire-level length caps — kept identical to the JS SDK so server
    // validation never trips on the native path.
    private static final int ERROR_CODE_MAX_LEN = 64;
    private static final int ERROR_MESSAGE_MAX_LEN = 500;
    private static final int BINARY_APP_VERSION_MAX_LEN = 40;

    private static final int CONNECT_TIMEOUT_MS = 10_000;
    private static final int READ_TIMEOUT_MS = 10_000;

    private static final ExecutorService EXECUTOR =
            Executors.newSingleThreadExecutor(runnable -> {
                Thread t = new Thread(runnable, "CodePushDeployReporter");
                t.setDaemon(true);
                return t;
            });

    private CodePushDeployReporter() {
        // utility
    }

    /**
     * Posts a single failure report. Returns immediately; HTTP runs on the
     * background executor.
     *
     * @param context           Used to read client unique ID + telemetry/data
     *                          transmission flags from SharedPreferences. Not
     *                          retained beyond this call.
     * @param serverUrl         Required. Server base URL (no trailing slash).
     * @param deploymentKey     Required. Release channel public ID.
     * @param binaryAppVersion  The client app's binary version (e.g. {@code 1.4.2}).
     *                          Surfaced as {@code binary_app_version} on the
     *                          wire and as the default {@code app_version} when
     *                          no failed package is supplied.
     * @param failedPackage     Optional. The package whose deployment failed;
     *                          contributes {@code label} and overrides
     *                          {@code app_version} with the package's
     *                          {@code appVersion} (target binary range).
     * @param errorCode         Recommended values from the FailureErrorCode
     *                          dictionary. Capped at 64 chars.
     *                          Null/empty → {@code "UNKNOWN"}.
     * @param errorMessage      Optional, free-form. Capped at 500 chars.
     * @param failedPhase       One of {@code download / install / runtime / unknown};
     *                          anything else coerced to {@code "unknown"}.
     */
    public static void reportFailure(
            final Context context,
            final String serverUrl,
            final String deploymentKey,
            final String binaryAppVersion,
            final JSONObject failedPackage,
            final String errorCode,
            final String errorMessage,
            final String failedPhase
    ) {
        if (context == null) {
            return;
        }
        if (TextUtils.isEmpty(serverUrl) || TextUtils.isEmpty(deploymentKey)) {
            return;
        }

        // Mirror the JS-side guard: bail out if either flag is off.
        SharedPreferences prefs = context.getApplicationContext()
                .getSharedPreferences(CodePushConstants.CODE_PUSH_PREFERENCES, 0);
        if (prefs.contains(CodePushConstants.TELEMETRY_ENABLED_KEY)
                && !prefs.getBoolean(CodePushConstants.TELEMETRY_ENABLED_KEY, true)) {
            return;
        }
        if (prefs.contains(CodePushConstants.DATA_TRANSMISSION_ENABLED_KEY)
                && !prefs.getBoolean(CodePushConstants.DATA_TRANSMISSION_ENABLED_KEY, true)) {
            return;
        }

        // Resolve client unique ID. CodePushNativeModule lazily generates one
        // on first ctor; mirror that behaviour so we work even when called
        // before the RN bridge (and hence the module) has been initialised.
        String clientUniqueId = prefs.getString(CodePushConstants.CLIENT_UNIQUE_ID_KEY, null);
        if (TextUtils.isEmpty(clientUniqueId)) {
            clientUniqueId = UUID.randomUUID().toString();
            prefs.edit().putString(CodePushConstants.CLIENT_UNIQUE_ID_KEY, clientUniqueId).apply();
        }

        final String resolvedClientUniqueId = clientUniqueId;
        final String normalizedErrorCode = normalizeErrorCode(errorCode);
        final String normalizedFailedPhase = normalizeFailedPhase(failedPhase);
        final String normalizedErrorMessage = truncate(errorMessage, ERROR_MESSAGE_MAX_LEN);
        final String normalizedBinaryAppVersion = truncate(binaryAppVersion, BINARY_APP_VERSION_MAX_LEN);

        final JSONObject body = buildBody(
                deploymentKey,
                resolvedClientUniqueId,
                normalizedBinaryAppVersion,
                failedPackage,
                normalizedErrorCode,
                normalizedErrorMessage,
                normalizedFailedPhase);
        if (body == null) {
            return;
        }

        final String url = serverUrl + PATH_REPORT_DEPLOY;

        EXECUTOR.execute(() -> postBlocking(url, body, normalizedErrorCode, normalizedFailedPhase));
    }

    private static JSONObject buildBody(
            String deploymentKey,
            String clientUniqueId,
            String binaryAppVersion,
            JSONObject failedPackage,
            String errorCode,
            String errorMessage,
            String failedPhase
    ) {
        try {
            JSONObject body = new JSONObject();
            body.put(KEY_DEPLOYMENT_KEY, deploymentKey);
            body.put(KEY_CLIENT_UNIQUE_ID, clientUniqueId);
            body.put(KEY_STATUS, STATUS_DEPLOYMENT_FAILED);

            // app_version mirrors the JS contract: defaults to the client
            // binary version, then gets overwritten to the failed package's
            // appVersion (target binary range) when a package is supplied.
            if (!TextUtils.isEmpty(binaryAppVersion)) {
                body.put(KEY_APP_VERSION, binaryAppVersion);
            }
            if (failedPackage != null) {
                String pkgLabel = failedPackage.optString(PKG_KEY_LABEL, null);
                String pkgAppVersion = failedPackage.optString(PKG_KEY_APP_VERSION, null);
                if (!TextUtils.isEmpty(pkgLabel)) {
                    body.put(KEY_LABEL, pkgLabel);
                }
                if (!TextUtils.isEmpty(pkgAppVersion)) {
                    body.put(KEY_APP_VERSION, pkgAppVersion);
                }
            }

            body.put(KEY_ERROR_CODE, errorCode);
            body.put(KEY_FAILED_PHASE, failedPhase);
            if (!TextUtils.isEmpty(errorMessage)) {
                body.put(KEY_ERROR_MESSAGE, errorMessage);
            }
            if (!TextUtils.isEmpty(binaryAppVersion)) {
                body.put(KEY_BINARY_APP_VERSION, binaryAppVersion);
            }
            return body;
        } catch (JSONException e) {
            CodePushUtils.log(TAG + "JSON build failed: " + e.getMessage());
            return null;
        }
    }

    private static void postBlocking(String url, JSONObject body, String errorCode, String failedPhase) {
        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) new URL(url).openConnection();
            conn.setRequestMethod("POST");
            conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
            conn.setReadTimeout(READ_TIMEOUT_MS);
            conn.setDoOutput(true);
            conn.setUseCaches(false);
            conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            conn.setRequestProperty("Accept", "application/json");

            byte[] bodyBytes = body.toString().getBytes(StandardCharsets.UTF_8);
            conn.setFixedLengthStreamingMode(bodyBytes.length);

            try (OutputStream os = conn.getOutputStream()) {
                os.write(bodyBytes);
            }

            int status = conn.getResponseCode();
            if (status >= 200 && status < 300) {
                CodePushUtils.log(TAG + "reported (errorCode=" + errorCode
                        + ", failedPhase=" + failedPhase + ")");
            } else {
                String responseBody = readErrorStream(conn);
                CodePushUtils.log(TAG + "HTTP " + status + " for " + url
                        + (responseBody != null ? " — " + responseBody : ""));
            }
        } catch (Exception e) {
            CodePushUtils.log(TAG + "HTTP error: " + e.getMessage());
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    private static String readErrorStream(HttpURLConnection conn) {
        InputStream errorStream = conn.getErrorStream();
        if (errorStream == null) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (Exception ignored) {
            // best-effort
        }
        return sb.length() == 0 ? null : sb.toString();
    }

    private static String normalizeErrorCode(String value) {
        String truncated = truncate(value, ERROR_CODE_MAX_LEN);
        return TextUtils.isEmpty(truncated) ? "UNKNOWN" : truncated;
    }

    private static String normalizeFailedPhase(String phase) {
        if ("download".equals(phase) || "install".equals(phase) || "runtime".equals(phase)) {
            return phase;
        }
        return "unknown";
    }

    private static String truncate(String value, int maxLen) {
        if (value == null || value.isEmpty()) {
            return null;
        }
        return value.length() > maxLen ? value.substring(0, maxLen) : value;
    }
}
