Skip to content

Conversation

@phoenixmariepornstaractress

Hello,
I have developed an updated and unified version of the subscription and purchase management system for Android apps, which includes advanced handling of subscriptions, user behavior analytics, refund prediction, and machine learning models to optimize revenue and trial conversion rates. This version is designed with readability, efficiency, and future extensibility in mind, providing developers with enhanced analytical and visualization tools. Merging these changes into your repository can improve code quality, automate data analysis, and offer advanced decision-making capabilities for the team. I hope you find these improvements valuable and consider integrating them into the main project.

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Single-file demo:
 * - Contains a corrected IInAppBillingService interface (Java-valid signatures)
 * - Provides a MockInAppBillingService with stubbed responses
 * - Provides a BillingDemoActivity with a programmatic, visually improved UI
 *
 * Note: This is a demo/stub. Replace the MockInAppBillingService with a real implementation
 * when integrating with actual billing APIs.
 */

package com.android.vending.billing;

import android.app.AlertDialog;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.method.ScrollingMovementMethod;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

/**
 * Single-file demo activity that contains:
 * - IInAppBillingService (corrected)
 * - A MockInAppBillingService implementation (stubs)
 * - A small UI to call several billing methods and display results
 */
public class BillingDemoActivity extends AppCompatActivity {

    // ---------------------------
    // Corrected interface (Java)
    // ---------------------------
    /**
     * Java interface for in-app billing service.
     * All "in" keywords from AIDL removed; methods use standard Java types.
     */
    interface IInAppBillingService {

        int isBillingSupported(int apiVersion, String packageName, String type);

        Bundle getSkuDetails(int apiVersion, String packageName, String type, Bundle skusBundle);

        Bundle getBuyIntent(int apiVersion,
                            String packageName,
                            String sku,
                            String type,
                            String developerPayload);

        Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);

        int consumePurchase(int apiVersion, String packageName, String purchaseToken);

        Bundle getBuyIntentV2(int apiVersion,
                              String packageName,
                              String sku,
                              String type,
                              String developerPayload);

        Bundle getPurchaseConfig(int apiVersion);

        Bundle getBuyIntentV3(
                int apiVersion,
                String packageName,
                String sku,
                String developerPayload,
                Bundle extraData);

        Bundle checkTrialSubscription(String packageName);

        Bundle getFeatureConfig();

        // ---------------------------------------------------------------------
        // 🔹 Added functions (corrected)
        // ---------------------------------------------------------------------

        Bundle getActiveSubscriptions(int apiVersion, String packageName, String userId);

        int acknowledgePurchase(int apiVersion, String packageName, String purchaseToken);

        Bundle getSkuOffers(int apiVersion, String packageName, String sku, Bundle extraParams);

        Bundle launchPriceChangeFlow(int apiVersion, String packageName, String sku, Bundle options);

        Bundle validatePurchase(int apiVersion, String packageName, String purchaseToken, Bundle validationData);

        Bundle getPurchaseHistory(int apiVersion, String packageName, String type, String continuationToken);

        int enableDeveloperMode(int apiVersion, String packageName, boolean enabled);

        Bundle getBillingDiagnostics(int apiVersion, String packageName);

        int cancelSubscription(int apiVersion, String packageName, String sku, String userId);

        Bundle changeSubscriptionPlan(int apiVersion,
                                      String packageName,
                                      String oldSku,
                                      String newSku,
                                      Bundle options);

        Bundle getAvailablePaymentMethods(int apiVersion, String packageName);

        int prefetchSkuDetails(int apiVersion, String packageName, Bundle skuList);
    }

    // -----------------------------------
    // Mock implementation for testing
    // -----------------------------------
    /**
     * Simple mock/stub implementation of IInAppBillingService.
     * Returns sample Bundles and integer response codes.
     */
    static class MockInAppBillingService implements IInAppBillingService {

        private final Handler mainHandler = new Handler(Looper.getMainLooper());

        @OverRide
        public int isBillingSupported(int apiVersion, String packageName, String type) {
            // 0 means OK in many billing APIs — use 0 for success
            return 0;
        }

        @OverRide
        public Bundle getSkuDetails(int apiVersion, String packageName, String type, Bundle skusBundle) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("DETAILS", "Sample SKU details for type=" + type + " pkg=" + packageName);
            return b;
        }

        @OverRide
        public Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, String developerPayload) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("BUY_INTENT", "intent://buy/" + sku + "?pkg=" + packageName);
            b.putString("DEVELOPER_PAYLOAD", developerPayload);
            return b;
        }

        @OverRide
        public Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("PURCHASES", "[{\"sku\":\"sample_monthly\",\"state\":\"active\"}]");
            b.putString("CONTINUATION_TOKEN", continuationToken == null ? "" : continuationToken);
            return b;
        }

        @OverRide
        public int consumePurchase(int apiVersion, String packageName, String purchaseToken) {
            // Return 0 for success
            return 0;
        }

        @OverRide
        public Bundle getBuyIntentV2(int apiVersion, String packageName, String sku, String type, String developerPayload) {
            // Forward to getBuyIntent for mock purposes
            return getBuyIntent(apiVersion, packageName, sku, type, developerPayload);
        }

        @OverRide
        public Bundle getPurchaseConfig(int apiVersion) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("CONFIG", "mock-config-v1");
            return b;
        }

        @OverRide
        public Bundle getBuyIntentV3(int apiVersion, String packageName, String sku, String developerPayload, Bundle extraData) {
            Bundle b = getBuyIntent(apiVersion, packageName, sku, "inapp", developerPayload);
            b.putBundle("EXTRA_DATA", extraData == null ? new Bundle() : extraData);
            return b;
        }

        @OverRide
        public Bundle checkTrialSubscription(String packageName) {
            Bundle b = new Bundle();
            b.putBoolean("HAS_TRIAL", true);
            b.putString("PACKAGE", packageName);
            return b;
        }

        @OverRide
        public Bundle getFeatureConfig() {
            Bundle b = new Bundle();
            b.putString("FEATURES", "mock-features-list");
            return b;
        }

        @OverRide
        public Bundle getActiveSubscriptions(int apiVersion, String packageName, String userId) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("ACTIVE_SUBSCRIPTIONS", "[{\"sku\":\"pro_monthly\",\"user\":\"" + userId + "\"}]");
            return b;
        }

        @OverRide
        public int acknowledgePurchase(int apiVersion, String packageName, String purchaseToken) {
            return 0; // success
        }

        @OverRide
        public Bundle getSkuOffers(int apiVersion, String packageName, String sku, Bundle extraParams) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("OFFERS", "[{\"offerId\":\"discount1\",\"sku\":\"" + sku + "\"}]");
            return b;
        }

        @OverRide
        public Bundle launchPriceChangeFlow(int apiVersion, String packageName, String sku, Bundle options) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("PRICE_CHANGE_STATUS", "price-change-flow-launched-for:" + sku);
            return b;
        }

        @OverRide
        public Bundle validatePurchase(int apiVersion, String packageName, String purchaseToken, Bundle validationData) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putBoolean("VALID", true);
            b.putString("PURCHASE_TOKEN", purchaseToken);
            return b;
        }

        @OverRide
        public Bundle getPurchaseHistory(int apiVersion, String packageName, String type, String continuationToken) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("HISTORY", "[{\"sku\":\"sample_one_time\",\"state\":\"consumed\"}]");
            return b;
        }

        @OverRide
        public int enableDeveloperMode(int apiVersion, String packageName, boolean enabled) {
            return enabled ? 0 : 1; // 0 success when enabling; 1 if disabled (mock)
        }

        @OverRide
        public Bundle getBillingDiagnostics(int apiVersion, String packageName) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("DIAGNOSTICS", "mock-diagnostics-ok");
            return b;
        }

        @OverRide
        public int cancelSubscription(int apiVersion, String packageName, String sku, String userId) {
            return 0; // success
        }

        @OverRide
        public Bundle changeSubscriptionPlan(int apiVersion, String packageName, String oldSku, String newSku, Bundle options) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("RESULT", "changed " + oldSku + " -> " + newSku);
            return b;
        }

        @OverRide
        public Bundle getAvailablePaymentMethods(int apiVersion, String packageName) {
            Bundle b = new Bundle();
            b.putInt("RESPONSE_CODE", 0);
            b.putString("PAYMENT_METHODS", "[\"card\",\"carrier\",\"paypal\"]");
            return b;
        }

        @OverRide
        public int prefetchSkuDetails(int apiVersion, String packageName, Bundle skuList) {
            // Pretend caching succeeded
            return 0;
        }
    }

    // --------------------------
    // Activity UI and behavior
    // --------------------------
    private IInAppBillingService billingService;

    @OverRide
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        billingService = new MockInAppBillingService();

        // Build a simple UI programmatically so everything stays in one file.
        ScrollView scroll = new ScrollView(this);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setPadding(dp(16), dp(16), dp(16), dp(16));
        root.setLayoutParams(new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        ));

        // Header
        TextView header = new TextView(this);
        header.setText("In-App Billing Demo");
        header.setTextSize(22f);
        header.setTypeface(Typeface.DEFAULT_BOLD);
        header.setPadding(0, 0, 0, dp(12));
        header.setGravity(Gravity.CENTER_HORIZONTAL);
        header.setTextColor(0xFF212121);
        root.addView(header, new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
        ));

        // A short subtitle / description
        TextView subtitle = new TextView(this);
        subtitle.setText("Mock billing service — tap any action to see sample results.");
        subtitle.setTextSize(14f);
        subtitle.setTextColor(0xFF666666);
        subtitle.setPadding(0, 0, 0, dp(12));
        subtitle.setGravity(Gravity.CENTER_HORIZONTAL);
        root.addView(subtitle);

        // Add buttons (card-like) for several actions
        root.addView(makeActionButton("Check Billing Support", new View.OnClickListener() {
            @OverRide
            public void onClick(View v) {
                int rc = billingService.isBillingSupported(3, getPackageName(), "inapp");
                showResult("isBillingSupported", "Response code: " + rc);
            }
        }));

        root.addView(makeActionButton("Get SKU Details", new View.OnClickListener() {
            @OverRide
            public void onClick(View v) {
                Bundle req = new Bundle();
                req.putStringArray("ITEM_ID_LIST", new String[]{"sample_one_time", "sample_monthly"});
                Bundle res = billingService.getSkuDetails(3, getPackageName(), "inapp", req);
                showBundle("getSkuDetails", res);
            }
        }));

        root.addView(makeActionButton("Get Purchases", new View.OnClickListener() {
            @OverRide
            public void onClick(View v) {
                Bundle res = billingService.getPurchases(3, getPackageName(), "inapp", null);
                showBundle("getPurchases", res);
            }
        }));

        root.addView(makeActionButton("Get Active Subscriptions", new View.OnClickListener() {
            @OverRide
            public void onClick(View v) {
                Bundle res = billingService.getActiveSubscriptions(3, getPackageName(), "user123");
                showBundle("getActiveSubscriptions", res);
            }
        }));

        root.addView(makeActionButton("Validate Purchase (mock)", new View.OnClickListener() {
            @OverRide
            public void onClick(View v) {
                Bundle valData = new Bundle();
                valData.putString("signature", "mock-signature");
                Bundle res = billingService.validatePurchase(3, getPackageName(), "token-abc-123", valData);
                showBundle("validatePurchase", res);
            }
        }));

        // Footer note
        TextView footer = new TextView(this);
        footer.setText("\nThis UI uses a mock billing service. Replace MockInAppBillingService with\na real service implementation for production.");
        footer.setTextSize(12f);
        footer.setTextColor(0xFF888888);
        footer.setPadding(0, dp(12), 0, 0);
        footer.setMovementMethod(new ScrollingMovementMethod());
        root.addView(footer);

        scroll.addView(root);
        setContentView(scroll);
    }

    // Helper that creates a rounded, shadowless "card" button for actions.
    private View makeActionButton(String label, View.OnClickListener onClick) {
        // container with rounded background
        LinearLayout container = new LinearLayout(this);
        container.setOrientation(LinearLayout.VERTICAL);
        container.setPadding(dp(12), dp(12), dp(12), dp(12));
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        );
        lp.setMargins(0, 0, 0, dp(12));
        container.setLayoutParams(lp);

        GradientDrawable bg = new GradientDrawable();
        bg.setCornerRadius(dp(10));
        bg.setColor(0xFFF7F7F7); // light card color
        container.setBackground(bg);

        // Title
        TextView title = new TextView(this);
        title.setText(label);
        title.setTextSize(16f);
        title.setTypeface(Typeface.DEFAULT_BOLD);
        title.setTextColor(0xFF111111);

        // Button
        Button b = new Button(this);
        b.setText("Run");
        b.setAllCaps(false);
        LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        );
        btnParams.gravity = Gravity.END;
        btnParams.topMargin = dp(8);
        b.setLayoutParams(btnParams);

        // Wire click: click on either card or the button triggers action
        container.setOnClickListener(onClick);
        b.setOnClickListener(onClick);

        container.addView(title);
        container.addView(b);

        return container;
    }

    // Helper: display bundle contents in an AlertDialog
    private void showBundle(String title, Bundle b) {
        StringBuilder sb = new StringBuilder();
        if (b == null) {
            sb.append("null");
        } else {
            for (String key : b.keySet()) {
                Object val = b.get(key);
                sb.append(key).append(": ").append(String.valueOf(val)).append("\n\n");
            }
        }
        showResult(title, sb.toString());
    }

    private void showResult(String title, String text) {
        // Show an AlertDialog with rounded message text
        TextView message = new TextView(this);
        message.setText(text);
        message.setTextSize(14f);
        message.setPadding(dp(12), dp(12), dp(12), dp(12));
        message.setTextColor(0xFF222222);

        new AlertDialog.Builder(this)
                .setTitle(title)
                .setView(message)
                .setPositiveButton("OK", null)
                .show();
    }

    private int dp(int d) {
        float scale = getResources().getDisplayMetrics().density;
        return Math.round(d * scale);
    }
}
package ir.cafebazaar.poolakey

/**
 * An inline extension that behaves like [takeIf], but also executes [andIfNot]
 * when the predicate condition fails.
 *
 * Example:
 * ```
 * val number = 5.takeIfOrElse(
 *     thisIsTrue = { it > 10 },
 *     andIfNot = { println("Number is too small!") }
 * )
 * // prints: Number is too small!
 * // returns: null
 * ```
 *
 * @param thisIsTrue Predicate to test on the receiver [T].
 * @param andIfNot Lambda to execute if [thisIsTrue] evaluates to false.
 * @return The receiver [T] if the predicate is true, otherwise `null`.
 */
internal inline fun <T> T.takeIfOrElse(
    thisIsTrue: (T) -> Boolean,
    andIfNot: () -> Unit
): T? = if (thisIsTrue(this)) this else {
    andIfNot()
    null
}

/**
 * Simple demo console app to show how [takeIfOrElse] works.
 * It prints results in a user-friendly, colored format.
 */
fun main() {
    // Terminal color codes for style
    val reset = "\u001B[0m"
    val blue = "\u001B[36m"
    val green = "\u001B[32m"
    val yellow = "\u001B[33m"
    val red = "\u001B[31m"
    val bold = "\u001B[1m"

    // Header UI
    println("$blue$bold==============================")
    println("   🧠 Poolakey takeIfOrElse Demo")
    println("==============================$reset\n")

    val input = "Hello"

    println("${yellow}Input Value:$reset \"$input\"")
    println("${yellow}Condition:$reset length > 10\n")

    val result = input.takeIfOrElse(
        thisIsTrue = { it.length > 10 },
        andIfNot = {
            println("${red}❌ Condition failed: String too short!$reset\n")
        }
    )

    if (result != null) {
        println("${green}✅ Success!$reset Value passed the test.")
        println("${yellow}Result:$reset \"$result\"")
    } else {
        println("${blue}ℹ️  Returned:$reset null (predicate not satisfied)")
    }

    println("\n$blue$bold==============================")
    println("         End of Demo")
    println("==============================$reset")
}
==============================
   🧠 Poolakey takeIfOrElse Demo
==============================

Input Value: "Hello"
Condition: length > 10

❌ Condition failed: String too short!

ℹ️  Returned: null (predicate not satisfied)

==============================
         End of Demo
==============================
package ir.cafebazaar.poolakey

/**
 * Represents the type of a purchase in the billing system.
 *
 * There are two main types:
 * - [IN_APP] → One-time purchases
 * - [SUBSCRIPTION] → Recurring payments
 */
internal enum class PurchaseType(val type: String) {
    IN_APP("inapp"),
    SUBSCRIPTION("subs");

    companion object {
        /**
         * Returns the [PurchaseType] matching the given [type] string, or `null` if invalid.
         *
         * Example:
         * ```
         * val result = PurchaseType.fromType("subs")  // SUBSCRIPTION
         * ```
         */
        fun fromType(type: String?): PurchaseType? {
            if (type.isNullOrBlank()) return null
            return values().firstOrNull { it.type.equals(type.trim(), ignoreCase = true) }
        }

        /**
         * Checks if the given [type] string is a valid purchase type.
         *
         * Example:
         * ```
         * val isValid = PurchaseType.isValidType("inapp")  // true
         * ```
         */
        fun isValidType(type: String?): Boolean = fromType(type) != null

        /**
         * Returns all available purchase type strings.
         *
         * Example:
         * ```
         * val all = PurchaseType.listAllTypes()  // ["inapp", "subs"]
         * ```
         */
        fun listAllTypes(): List<String> = values().map { it.type }
    }

    /**
     * Returns a friendly display name for the type (for UI or logs).
     *
     * Example:
     * ```
     * PurchaseType.IN_APP.displayName()  // "In-App Purchase"
     * ```
     */
    fun displayName(): String = when (this) {
        IN_APP -> "In-App Purchase"
        SUBSCRIPTION -> "Subscription"
    }

    /**
     * Returns true if this is a subscription type.
     */
    fun isSubscription(): Boolean = this == SUBSCRIPTION

    /**
     * Returns true if this is an in-app (one-time) purchase type.
     */
    fun isInApp(): Boolean = this == IN_APP
}

// ---------------------------------------------------------------------
// Interactive, polished console UI demo (single-file)
// ---------------------------------------------------------------------
fun main() {
    // ANSI colors for nicer terminal UI (works in most terminals)
    val RESET = "\u001B[0m"
    val BOLD = "\u001B[1m"
    val CYAN = "\u001B[36m"
    val GREEN = "\u001B[32m"
    val YELLOW = "\u001B[33m"
    val RED = "\u001B[31m"
    val MAGENTA = "\u001B[35m"

    fun header() {
        println("${CYAN + BOLD}==============================================")
        println("   🛒  Poolakey PurchaseType Utility Demo")
        println("==============================================$RESET")
    }

    fun footer() {
        println()
        println("${CYAN}Thank you for trying the demo — exit with 'q' or Ctrl+C.$RESET")
    }

    header()

    while (true) {
        println()
        println("${YELLOW}Available actions:${RESET}")
        println("  1) List all purchase types")
        println("  2) Convert string -> PurchaseType")
        println("  3) Show display names")
        println("  4) Type checks (isInApp / isSubscription)")
        println("  q) Quit")
        print("\nSelect an action (1-4 or q): ")

        val choice = readLine()?.trim()?.lowercase()
        when (choice) {
            "1" -> {
                val types = PurchaseType.listAllTypes()
                println()
                println("${MAGENTA}🔹 All purchase types:${RESET} ${types.joinToString(", ")}")
            }
            "2" -> {
                print("Enter a type string (e.g. \"inapp\" or \"subs\"): ")
                val input = readLine()?.trim()
                val p = PurchaseType.fromType(input)
                println()
                if (p != null) {
                    println("${GREEN}✅ Parsed successfully:${RESET} ${p.name} (${p.type}) — ${p.displayName()}")
                } else {
                    println("${RED}❌ Invalid purchase type:${RESET} \"${input ?: ""}\"")
                    println("    Valid values: ${PurchaseType.listAllTypes().joinToString(", ")}")
                }
            }
            "3" -> {
                println()
                println("${MAGENTA}🎨 Display names:${RESET}")
                PurchaseType.values().forEach { t ->
                    println("  • ${t.type} → ${t.displayName()}")
                }
            }
            "4" -> {
                println()
                println("Check a type:")
                print("Enter one of (${PurchaseType.listAllTypes().joinToString(", ")}): ")
                val input = readLine()?.trim()
                val p = PurchaseType.fromType(input)
                if (p == null) {
                    println("${RED}❌ Unknown type: ${input ?: "\"\""}${RESET}")
                } else {
                    val checks = buildString {
                        append("${GREEN}✔ ${p.name}${RESET} checks:\n")
                        append("   - isInApp(): ${if (p.isInApp()) "${BOLD}true" else "false"}\n")
                        append("   - isSubscription(): ${if (p.isSubscription()) "${BOLD}true" else "false"}")
                    }
                    println(checks)
                }
            }
            "q", "quit", "exit" -> {
                println()
                println("${CYAN}Exiting demo...$RESET")
                footer()
                return
            }
            else -> {
                println()
                println("${RED}⚠️  Invalid selection. Please choose 1-4 or q.$RESET")
            }
        }
    }
}
package ir.cafebazaar.poolakey

import android.content.Intent
import ir.cafebazaar.poolakey.callback.PurchaseCallback
import ir.cafebazaar.poolakey.config.SecurityCheck
import ir.cafebazaar.poolakey.constant.BazaarIntent
import ir.cafebazaar.poolakey.exception.PurchaseHijackedException
import ir.cafebazaar.poolakey.mapper.RawDataToPurchaseInfo
import ir.cafebazaar.poolakey.security.PurchaseVerifier
import java.security.InvalidKeyException
import java.security.NoSuchAlgorithmException
import java.security.SignatureException
import java.security.spec.InvalidKeySpecException

internal class PurchaseResultParser(
    private val rawDataToPurchaseInfo: RawDataToPurchaseInfo,
    private val purchaseVerifier: PurchaseVerifier
) {

    // ANSI color codes for terminal logs
    private val RESET = "\u001B[0m"
    private val GREEN = "\u001B[32m"
    private val RED = "\u001B[31m"
    private val YELLOW = "\u001B[33m"
    private val CYAN = "\u001B[36m"
    private val BOLD = "\u001B[1m"

    fun handleReceivedResult(
        securityCheck: SecurityCheck,
        data: Intent?,
        purchaseCallback: PurchaseCallback.() -> Unit
    ) {
        val code = data?.extras?.get(BazaarIntent.RESPONSE_CODE)
        if (code == BazaarIntent.RESPONSE_RESULT_OK) {
            parseResult(securityCheck, data, purchaseCallback)
        } else {
            logError("Response code invalid: $code")
            PurchaseCallback().apply(purchaseCallback)
                .purchaseFailed
                .invoke(IllegalStateException("Response code is not valid"))
        }
    }

    private fun parseResult(
        securityCheck: SecurityCheck,
        data: Intent?,
        purchaseCallback: PurchaseCallback.() -> Unit
    ) {
        val purchaseData = data?.getStringExtra(BazaarIntent.RESPONSE_PURCHASE_DATA)
        val dataSignature = data?.getStringExtra(BazaarIntent.RESPONSE_SIGNATURE_DATA)

        if (purchaseData != null && dataSignature != null) {
            validatePurchase(
                securityCheck = securityCheck,
                purchaseData = purchaseData,
                dataSignature = dataSignature,
                purchaseIsValid = {
                    val purchaseInfo = rawDataToPurchaseInfo.mapToPurchaseInfo(
                        purchaseData,
                        dataSignature
                    )
                    logSuccess("Purchase validated successfully: $purchaseInfo")
                    PurchaseCallback().apply(purchaseCallback)
                        .purchaseSucceed
                        .invoke(purchaseInfo)
                },
                purchaseIsNotValid = { throwable ->
                    logError("Purchase validation failed: ${throwable.message}")
                    PurchaseCallback().apply(purchaseCallback)
                        .purchaseFailed
                        .invoke(throwable)
                }
            )
        } else {
            logError("Received invalid purchase data or signature")
            PurchaseCallback().apply(purchaseCallback)
                .purchaseFailed
                .invoke(IllegalStateException("Received data is not valid"))
        }
    }

    private inline fun validatePurchase(
        securityCheck: SecurityCheck,
        purchaseData: String,
        dataSignature: String,
        purchaseIsValid: () -> Unit,
        purchaseIsNotValid: (Throwable) -> Unit
    ) {
        if (securityCheck is SecurityCheck.Enable) {
            try {
                val isPurchaseValid = purchaseVerifier.verifyPurchase(
                    securityCheck.rsaPublicKey,
                    purchaseData,
                    dataSignature
                )
                if (isPurchaseValid) {
                    purchaseIsValid.invoke()
                } else {
                    purchaseIsNotValid.invoke(PurchaseHijackedException())
                }
            } catch (e: Exception) {
                purchaseIsNotValid.invoke(e)
            }
        } else {
            purchaseIsValid.invoke()
        }
    }

    // -------------------- Additional Helper Functions --------------------

    fun parsePurchaseSafe(data: Intent?): Result<Any> = try {
        val purchaseData = data?.getStringExtra(BazaarIntent.RESPONSE_PURCHASE_DATA)
        val signature = data?.getStringExtra(BazaarIntent.RESPONSE_SIGNATURE_DATA)
        if (purchaseData != null && signature != null) {
            val info = rawDataToPurchaseInfo.mapToPurchaseInfo(purchaseData, signature)
            logSuccess("Parsed purchase info: $info")
            Result.success(info)
        } else {
            logError("Invalid purchase data")
            Result.failure(IllegalStateException("Invalid purchase data"))
        }
    } catch (e: Exception) {
        logError("Exception parsing purchase: ${e.message}")
        Result.failure(e)
    }

    fun logPurchaseInfo(data: Intent?) {
        parsePurchaseSafe(data).onSuccess { info ->
            println("$GREEN✅ Purchase info: $info$RESET")
        }.onFailure { throwable ->
            println("$RED❌ Failed to parse purchase info: ${throwable.message}$RESET")
        }
    }

    fun handleWithRetry(
        securityCheck: SecurityCheck,
        data: Intent?,
        purchaseCallback: PurchaseCallback.() -> Unit,
        maxRetries: Int = 3
    ) {
        var attempts = 0
        fun tryValidate() {
            attempts++
            handleReceivedResult(securityCheck, data) {
                this.purchaseFailed = { throwable ->
                    if (attempts < maxRetries) {
                        println("$YELLOW⚠️ Retry attempt $attempts due to: ${throwable.message}$RESET")
                        tryValidate()
                    } else {
                        purchaseCallback()
                    }
                }
                this.purchaseSucceed = purchaseCallback().purchaseSucceed
            }
        }
        tryValidate()
    }

    fun handleMultiplePurchases(
        securityCheck: SecurityCheck,
        dataList: List<Intent?>,
        purchaseCallback: PurchaseCallback.() -> Unit
    ) {
        dataList.forEach { intent ->
            handleReceivedResult(securityCheck, intent, purchaseCallback)
        }
    }

    fun isPurchaseRevoked(data: Intent?): Boolean {
        val status = data?.extras?.getInt(BazaarIntent.RESPONSE_PURCHASE_STATE, -1)
        val revoked = status == BazaarIntent.PURCHASE_STATE_CANCELED
        if (revoked) logError("Purchase has been revoked or canceled")
        return revoked
    }

    // -------------------- Logging Helpers --------------------

    private fun logError(message: String) {
        println("$RED$BOLD❌ ERROR: $message$RESET")
    }

    private fun logSuccess(message: String) {
        println("$GREEN$BOLD✅ SUCCESS: $message$RESET")
    }

    private fun logInfo(message: String) {
        println("$CYANℹ️ INFO: $message$RESET")
    }
}
package ir.cafebazaar.poolakey

import android.content.Intent
import android.util.Log
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts

internal class PaymentLauncher private constructor(
    val activityLauncher: ActivityResultLauncher<Intent>,
    val intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
) {

    companion object {
        private const val TAG = "PaymentLauncher"

        // ANSI color codes for terminal (logs)
        private const val RESET = "\u001B[0m"
        private const val GREEN = "\u001B[32m"
        private const val RED = "\u001B[31m"
        private const val YELLOW = "\u001B[33m"
        private const val CYAN = "\u001B[36m"
        private const val BOLD = "\u001B[1m"
    }

    class Builder(
        private val registry: ActivityResultRegistry,
        private val onActivityResult: (ActivityResult) -> Unit
    ) {

        fun build(): PaymentLauncher {
            val activityLauncher = registry.register(
                BillingConnection.PAYMENT_SERVICE_KEY,
                ActivityResultContracts.StartActivityForResult(),
                onActivityResult::invoke
            )

            val intentSenderLauncher = registry.register(
                BillingConnection.PAYMENT_SERVICE_KEY,
                ActivityResultContracts.StartIntentSenderForResult(),
                onActivityResult::invoke
            )

            return PaymentLauncher(activityLauncher, intentSenderLauncher)
        }
    }

    fun unregister() {
        activityLauncher.unregister()
        intentSenderLauncher.unregister()
        logInfo("Launchers unregistered")
    }

    // -------------------- Additional Helper Functions --------------------

    /** Launch a payment via Intent */
    fun launchPayment(intent: Intent) {
        try {
            activityLauncher.launch(intent)
            logSuccess("Payment launched via Intent")
        } catch (e: Exception) {
            logError("Failed to launch payment Intent: ${e.message}")
        }
    }

    /** Launch a payment via IntentSenderRequest */
    fun launchPayment(intentSenderRequest: IntentSenderRequest) {
        try {
            intentSenderLauncher.launch(intentSenderRequest)
            logSuccess("Payment launched via IntentSenderRequest")
        } catch (e: Exception) {
            logError("Failed to launch payment IntentSenderRequest: ${e.message}")
        }
    }

    /** Check if launchers are currently registered */
    fun areLaunchersRegistered(): Boolean {
        return try {
            activityLauncher.isEnabled && intentSenderLauncher.isEnabled
        } catch (e: Exception) {
            false
        }
    }

    /** Safely relaunch payment if launchers were unregistered */
    fun relaunchPayment(
        registry: ActivityResultRegistry,
        onActivityResult: (ActivityResult) -> Unit,
        intent: Intent? = null,
        intentSenderRequest: IntentSenderRequest? = null
    ) {
        logInfo("Relaunching payment...")
        val builder = Builder(registry, onActivityResult)
        val newLauncher = builder.build()
        intent?.let { newLauncher.launchPayment(it) }
        intentSenderRequest?.let { newLauncher.launchPayment(it) }
    }

    /** Launch payment and log results for debugging */
    fun launchPaymentWithLogging(intent: Intent) {
        logInfo("Attempting to launch payment...")
        launchPayment(intent)
        logInfo("Payment launch request complete")
    }

    // -------------------- Logging Helpers --------------------
    private fun logError(message: String) {
        Log.e(TAG, "$RED$BOLD❌ ERROR: $message$RESET")
    }

    private fun logSuccess(message: String) {
        Log.d(TAG, "$GREEN$BOLD✅ SUCCESS: $message$RESET")
    }

    private fun logInfo(message: String) {
        Log.i(TAG, "$CYANℹ️ INFO: $message$RESET")
    }
}
package ir.cafebazaar.poolakey

import android.content.Context
import androidx.activity.result.ActivityResultRegistry
import ir.cafebazaar.poolakey.billing.query.QueryFunction
import ir.cafebazaar.poolakey.billing.skudetail.GetSkuDetailFunction
import ir.cafebazaar.poolakey.billing.trialsubscription.CheckTrialSubscriptionFunction
import ir.cafebazaar.poolakey.callback.*
import ir.cafebazaar.poolakey.config.PaymentConfiguration
import ir.cafebazaar.poolakey.mapper.RawDataToPurchaseInfo
import ir.cafebazaar.poolakey.request.PurchaseRequest
import ir.cafebazaar.poolakey.security.PurchaseVerifier
import ir.cafebazaar.poolakey.thread.BackgroundThread
import ir.cafebazaar.poolakey.thread.MainThread
import ir.cafebazaar.poolakey.thread.PoolakeyThread

class Payment(
    context: Context,
    config: PaymentConfiguration
) {

    private val backgroundThread: PoolakeyThread<Runnable> = BackgroundThread()
    private val mainThread: PoolakeyThread<() -> Unit> = MainThread()

    private val purchaseVerifier = PurchaseVerifier()
    private val rawDataToPurchaseInfo = RawDataToPurchaseInfo()

    private val queryFunction = QueryFunction(
        rawDataToPurchaseInfo,
        purchaseVerifier,
        config,
        mainThread,
    )

    private val getSkuFunction = GetSkuDetailFunction(
        context,
        mainThread
    )

    private val checkTrialSubscriptionFunction = CheckTrialSubscriptionFunction(
        context,
        mainThread
    )

    private val purchaseResultParser = PurchaseResultParser(rawDataToPurchaseInfo, purchaseVerifier)

    private val connection = BillingConnection(
        context = context,
        paymentConfiguration = config,
        queryFunction = queryFunction,
        backgroundThread = backgroundThread,
        skuDetailFunction = getSkuFunction,
        purchaseResultParser = purchaseResultParser,
        checkTrialSubscriptionFunction = checkTrialSubscriptionFunction,
        mainThread = mainThread
    )

    fun connect(callback: ConnectionCallback.() -> Unit): Connection {
        return connection.startConnection(callback)
    }

    fun purchaseProduct(
        registry: ActivityResultRegistry,
        request: PurchaseRequest,
        callback: PurchaseCallback.() -> Unit
    ) {
        connection.purchase(registry, request, PurchaseType.IN_APP, callback)
    }

    fun consumeProduct(purchaseToken: String, callback: ConsumeCallback.() -> Unit) {
        connection.consume(purchaseToken, callback)
    }

    fun subscribeProduct(
        registry: ActivityResultRegistry,
        request: PurchaseRequest,
        callback: PurchaseCallback.() -> Unit
    ) {
        connection.purchase(registry, request, PurchaseType.SUBSCRIPTION, callback)
    }

    fun getPurchasedProducts(callback: PurchaseQueryCallback.() -> Unit) {
        connection.queryPurchasedProducts(PurchaseType.IN_APP, callback)
    }

    fun getSubscribedProducts(callback: PurchaseQueryCallback.() -> Unit) {
        connection.queryPurchasedProducts(PurchaseType.SUBSCRIPTION, callback)
    }

    fun getInAppSkuDetails(
        skuIds: List<String>,
        callback: GetSkuDetailsCallback.() -> Unit
    ) {
        connection.getSkuDetail(PurchaseType.IN_APP, skuIds, callback)
    }

    fun getSubscriptionSkuDetails(
        skuIds: List<String>,
        callback: GetSkuDetailsCallback.() -> Unit
    ) {
        connection.getSkuDetail(PurchaseType.SUBSCRIPTION, skuIds, callback)
    }

    fun checkTrialSubscription(
        callback: CheckTrialSubscriptionCallback.() -> Unit
    ) {
        connection.checkTrialSubscription(callback)
    }


    // -------------------- Additional Helper Functions --------------------

    fun refreshConnection(callback: ConnectionCallback.() -> Unit) {
        println("🔄 Refreshing connection to billing service...")
        connection.disconnect()
        connection.startConnection(callback)
    }

    fun isProductPurchased(sku: String, callback: (Boolean) -> Unit) {
        getPurchasedProducts {
            onQueryCompleted = { purchasedList ->
                val purchased = purchasedList.any { it.sku == sku }
                println("🔍 SKU '$sku' purchased? $purchased")
                callback(purchased)
            }
        }
    }

    fun isSubscriptionActive(sku: String, callback: (Boolean) -> Unit) {
        getSubscribedProducts {
            onQueryCompleted = { subscribedList ->
                val active = subscribedList.any { it.sku == sku && it.isAutoRenewing }
                println("🔍 Subscription '$sku' active? $active")
                callback(active)
            }
        }
    }

    fun restorePurchases(
        onRestoreCompleted: (purchasedProducts: List<String>, subscriptions: List<String>) -> Unit
    ) {
        val restoredProducts = mutableListOf<String>()
        val restoredSubscriptions = mutableListOf<String>()

        getPurchasedProducts {
            onQueryCompleted = { purchasedList ->
                restoredProducts.addAll(purchasedList.map { it.sku })

                getSubscribedProducts {
                    onQueryCompleted = { subscribedList ->
                        restoredSubscriptions.addAll(subscribedList.map { it.sku })
                        println("✅ Restored purchases: $restoredProducts")
                        println("🎉 Restored subscriptions: $restoredSubscriptions")
                        onRestoreCompleted(restoredProducts, restoredSubscriptions)
                    }
                }
            }
        }
    }

    fun logAllPurchasesAndSubscriptions() {
        getPurchasedProducts {
            onQueryCompleted = { purchasedList ->
                println("📦 Purchased products:")
                purchasedList.forEach { println(" - SKU: ${it.sku}, Token: ${it.purchaseToken}") }

                getSubscribedProducts {
                    onQueryCompleted = { subscribedList ->
                        println("✨ Active subscriptions:")
                        subscribedList.forEach { println(" - SKU: ${it.sku}, AutoRenew: ${it.isAutoRenewing}") }
                    }
                }
            }
        }
    }

    fun prefetchSkuDetails(
        inAppSkuIds: List<String> = emptyList(),
        subscriptionSkuIds: List<String> = emptyList(),
        callback: () -> Unit = {}
    ) {
        var pendingCalls = 0

        fun checkDone() {
            pendingCalls--
            if (pendingCalls <= 0) callback()
        }

        if (inAppSkuIds.isNotEmpty()) {
            pendingCalls++
            getInAppSkuDetails(inAppSkuIds) { onSkuDetailsReceived = { checkDone() } }
        }

        if (subscriptionSkuIds.isNotEmpty()) {
            pendingCalls++
            getSubscriptionSkuDetails(subscriptionSkuIds) { onSkuDetailsReceived = { checkDone() } }
        }

        if (pendingCalls == 0) callback()
    }
}
package ir.cafebazaar.poolakey

import android.app.AlertDialog
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import java.text.DateFormat
import java.util.Date
import java.util.Locale

/**
 * Package / version helper utilities for Poolakey.
 *
 * - Safe retrieval of PackageInfo
 * - SDK-aware version code reading
 * - Convenience checks and nicely formatted outputs
 * - A small native dialog helper to display package info in a visually friendly way
 */

/**
 * Safely retrieves [PackageInfo] for the given package name.
 */
internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
    val packageManager = context.packageManager
    packageManager.getPackageInfo(packageName, 0)
} catch (ignored: Exception) {
    null
}

/**
 * Returns the SDK-aware version code from the [PackageInfo].
 */
@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(packageInfo: PackageInfo): Long {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        packageInfo.longVersionCode
    } else {
        packageInfo.versionCode.toLong()
    }
}

// -------------------- Additional Utility Functions --------------------

/**
 * Retrieves the version name of the given package, or null if unavailable.
 */
internal fun getVersionName(context: Context, packageName: String): String? {
    return getPackageInfo(context, packageName)?.versionName
}

/**
 * Checks whether a given package is installed on the device.
 */
internal fun isPackageInstalled(context: Context, packageName: String): Boolean {
    return try {
        context.packageManager.getPackageInfo(packageName, 0)
        true
    } catch (e: PackageManager.NameNotFoundException) {
        false
    } catch (e: Exception) {
        false
    }
}

/**
 * Returns true if the given package's version is greater than or equal to [minVersionCode].
 */
internal fun isVersionAtLeast(context: Context, packageName: String, minVersionCode: Long): Boolean {
    val info = getPackageInfo(context, packageName)
    val currentCode = info?.let { sdkAwareVersionCode(it) } ?: return false
    return currentCode >= minVersionCode
}

/**
 * Logs detailed package info for debugging purposes (formatted).
 */
internal fun logPackageInfo(context: Context, packageName: String) {
    val info = getPackageInfo(context, packageName)
    if (info == null) {
        Log.w("Poolakey", "⚠️ Package $packageName not found or inaccessible.")
        return
    }
    val versionName = info.versionName ?: "N/A"
    val versionCode = sdkAwareVersionCode(info)
    val first = formatTime(info.firstInstallTime)
    val last = formatTime(info.lastUpdateTime)

    val message = """
        📦 Package Info:
        • Package Name: $packageName
        • Version Name: $versionName
        • Version Code: $versionCode
        • First Install: $first
        • Last Update : $last
    """.trimIndent()

    Log.d("Poolakey", message)
}

/**
 * Returns the first install time of the app in milliseconds.
 */
internal fun getFirstInstallTime(context: Context, packageName: String): Long? {
    return getPackageInfo(context, packageName)?.firstInstallTime
}

/**
 * Returns the last update time of the app in milliseconds.
 */
internal fun getLastUpdateTime(context: Context, packageName: String): Long? {
    return getPackageInfo(context, packageName)?.lastUpdateTime
}

/**
 * Compares two installed app versions by version code.
 * @return positive if [packageName1] > [packageName2], negative if less, 0 if equal or unknown.
 */
internal fun compareAppVersions(context: Context, packageName1: String, packageName2: String): Int {
    val info1 = getPackageInfo(context, packageName1)
    val info2 = getPackageInfo(context, packageName2)
    if (info1 == null || info2 == null) return 0
    val code1 = sdkAwareVersionCode(info1)
    val code2 = sdkAwareVersionCode(info2)
    return code1.compareTo(code2)
}

/**
 * Returns human-readable package info summary (for UI or logs).
 */
internal fun getPackageSummary(context: Context, packageName: String): String {
    val info = getPackageInfo(context, packageName)
    return if (info == null) {
        "Package \"$packageName\" is not installed."
    } else {
        val versionName = info.versionName ?: "Unknown"
        val versionCode = sdkAwareVersionCode(info)
        val installTime = formatTime(info.firstInstallTime)
        val updateTime = formatTime(info.lastUpdateTime)
        """
        📦 $packageName
        • Version: $versionName ($versionCode)
        • Installed: $installTime
        • Updated:   $updateTime
        """.trimIndent()
    }
}

/**
 * Returns a short, visually-appealing summary (single-line) useful for compact UIs or logs.
 */
internal fun getPackageShortSummary(context: Context, packageName: String): String {
    val info = getPackageInfo(context, packageName)
    return if (info == null) {
        "⚠️ $packageName — not installed"
    } else {
        val versionName = info.versionName ?: "?"
        val versionCode = sdkAwareVersionCode(info)
        "📦 $packageName — $versionName ($versionCode)"
    }
}

/**
 * Shows a simple native dialog with pretty package information.
 * Uses android.app.AlertDialog so it works with plain Context (but passing an Activity Context
 * is recommended so dialog is themed correctly).
 */
internal fun showPackageInfoDialog(context: Context, packageName: String) {
    val info = getPackageInfo(context, packageName)
    val title = "Package Info"
    val message = if (info == null) {
        "Package \"$packageName\" is not installed on this device."
    } else {
        val versionName = info.versionName ?: "Unknown"
        val versionCode = sdkAwareVersionCode(info)
        val installed = formatTime(info.firstInstallTime)
        val updated = formatTime(info.lastUpdateTime)
        """
        📦 $packageName

        • Version: $versionName
        • Version Code: $versionCode

        • Installed: $installed
        • Updated:   $updated
        """.trimIndent()
    }

    // Build and show the dialog (safe: will use application context fallback).
    try {
        AlertDialog.Builder(context)
            .setTitle(title)
            .setMessage(message)
            .setPositiveButton("OK", null)
            .show()
    } catch (e: Exception) {
        // If showing a dialog fails (e.g., non-activity context), fall back to logging.
        Log.w("Poolakey", "Unable to show dialog for $packageName: ${e.message}")
        Log.d("Poolakey", message)
    }
}

// -------------------- Helpers --------------------

private fun formatTime(epochMillis: Long): String {
    return try {
        if (epochMillis <= 0L) "N/A"
        else {
            val df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault())
            df.format(Date(epochMillis))
        }
    } catch (e: Exception) {
        epochMillis.toString()
    }
}
package ir.cafebazaar.poolakey

import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

/**
 * Represents the current connection state between the app and Bazaar billing service.
 * Extended with useful helpers for logging, UI binding and reactive adapters.
 */
sealed class ConnectionState {

    /** Indicates that a connection has been successfully established. */
    object Connected : ConnectionState()

    /** Indicates that the connection attempt has failed. */
    object FailedToConnect : ConnectionState()

    /** Indicates that the service has been disconnected. */
    object Disconnected : ConnectionState()

    // -------------------- Basic queries --------------------

    /** Returns `true` if the current state represents an active connection. */
    fun isConnected(): Boolean = this is Connected

    /** Returns `true` if the current state represents a disconnection. */
    fun isDisconnected(): Boolean = this is Disconnected

    /** Returns `true` if the current state represents a failed connection attempt. */
    fun isFailed(): Boolean = this is FailedToConnect

    /** Provides a human-readable description of the current connection state. */
    fun getDescription(): String = when (this) {
        Connected -> "✅ Connected to Bazaar billing service."
        FailedToConnect -> "❌ Failed to connect to Bazaar billing service."
        Disconnected -> "⚠️ Disconnected from Bazaar billing service."
    }

    /** Converts the connection state to a short status code string. */
    fun toStatusCode(): String = when (this) {
        Connected -> "CONNECTED"
        FailedToConnect -> "FAILED"
        Disconnected -> "DISCONNECTED"
    }

    /** Returns a simple emoji-based representation for logs or light UIs. */
    fun getEmoji(): String = when (this) {
        Connected -> "🟢"
        FailedToConnect -> "🔴"
        Disconnected -> "🟡"
    }

    // -------------------- Logging & diagnostics --------------------

    /** Prints a formatted debug log describing the current state. */
    fun logState(tag: String = "PoolakeyConnection") {
        android.util.Log.d(tag, "Connection State: ${toStatusCode()} - ${getDescription()}")
    }

    /** Returns a compact JSON-like representation (for logs or diagnostics). */
    fun toJsonString(): String {
        return """{
            "state": "${toStatusCode()}",
            "description": "${getDescription()}",
            "emoji": "${getEmoji()}",
            "reconnectAllowed": ${shouldAttemptReconnect()},
            "stability": ${getConnectionStabilityScore()}
        }""".trimIndent()
    }

    // -------------------- UI helpers --------------------

    /**
     * Maps the current state to a color code for UI display.
     * Uses Color.parseColor for clear, portable values.
     */
    @ColorInt
    fun getStateColor(): Int = when (this) {
        Connected -> Color.parseColor("#4CAF50")   // Green
        FailedToConnect -> Color.parseColor("#F44336") // Red
        Disconnected -> Color.parseColor("#FFC107") // Amber
    }

    /** Builds a short formatted UI-friendly string. Example: "🟢 Connected (Active)" */
    fun getUiLabel(): String = when (this) {
        Connected -> "${getEmoji()} Connected (Active)"
        FailedToConnect -> "${getEmoji()} Connection Failed"
        Disconnected -> "${getEmoji()} Disconnected"
    }

    /**
     * Styles a TextView to represent the connection state as a pill/badge.
     *
     * Example usage (on main thread):
     *   connectionState.bindToStatusView(statusTextView, context)
     */
    fun bindToStatusView(textView: TextView, context: Context) {
        // text
        textView.text = getUiLabel()

        // color
        val color = getStateColor()
        textView.setTextColor(Color.WHITE)

        // typeface and size
        textView.setTypeface(Typeface.DEFAULT_BOLD)
        textView.textSize = 14f

        // padding (left, top, right, bottom)
        val horizontal = dpToPx(context, 12)
        val vertical = dpToPx(context, 6)
        textView.setPadding(horizontal, vertical, horizontal, vertical)

        // rounded background (pill) with appropriate color
        val drawable = GradientDrawable()
        drawable.shape = GradientDrawable.RECTANGLE
        drawable.cornerRadius = dpToPx(context, 20).toFloat()
        // use semi-strong color for background; keep text white for contrast
        drawable.setColor(color)
        textView.background = drawable
    }

    /**
     * Applies a lighter badge-style background and colored text (inverse of bindToStatusView).
     * Useful when you want colored text on transparent/light background.
     */
    fun styleTextViewAsBadge(textView: TextView, context: Context) {
        val color = getStateColor()
        textView.text = getUiLabel()
        textView.setTextColor(color)
        textView.setTypeface(Typeface.DEFAULT_BOLD)
        textView.textSize = 13f
        val horizontal = dpToPx(context, 10)
        val vertical = dpToPx(context, 4)
        textView.setPadding(horizontal, vertical, horizontal, vertical)

        val drawable = GradientDrawable()
        drawable.shape = GradientDrawable.RECTANGLE
        drawable.cornerRadius = dpToPx(context, 16).toFloat()
        // light translucent background
        drawable.setColor(adjustAlpha(color, 0.12f))
        textView.background = drawable
    }

    // -------------------- Connection logic helpers --------------------

    /**
     * Suggests whether the system should attempt to reconnect.
     * Returns true if the current state is FailedToConnect or Disconnected.
     */
    fun shouldAttemptReconnect(): Boolean = when (this) {
        Connected -> false
        FailedToConnect, Disconnected -> true
    }

    /**
     * Returns a next recommended state based on current state and external conditions.
     */
    fun getNextSuggestedState(): ConnectionState = when (this) {
        Connected -> Disconnected
        FailedToConnect, Disconnected -> Connected
    }

    /**
     * Returns a notification-friendly message for the current state.
     */
    fun toNotificationMessage(): String = when (this) {
        Connected -> "🟢 Connection established with Bazaar."
        FailedToConnect -> "🔴 Unable to reach Bazaar servers. Please try again."
        Disconnected -> "🟡 Connection lost. Attempting to reconnect..."
    }

    /**
     * Returns an estimated connection stability score (0–100).
     */
    fun getConnectionStabilityScore(): Int = when (this) {
        Connected -> 100
        FailedToConnect -> 25
        Disconnected -> 50
    }

    // -------------------- Reactive adapters --------------------

    /**
     * Converts the state into a LiveData value — helpful for UI observation.
     * Note: returned LiveData contains the single value of this state.
     */
    fun asLiveData(): LiveData<ConnectionState> {
        val liveData = MutableLiveData<ConnectionState>()
        liveData.value = this
        return liveData
    }

    /**
     * Converts the state into a StateFlow value — useful for reactive UIs.
     * Note: returned StateFlow contains this state as its initial value.
     */
    fun asStateFlow(): StateFlow<ConnectionState> {
        return MutableStateFlow(this)
    }

    // -------------------- Utility helpers --------------------

    private fun dpToPx(context: Context, dp: Int): Int {
        val scale = context.resources.displayMetrics.density
        return (dp * scale + 0.5f).toInt()
    }

    private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
        val alpha = (Color.alpha(color) * factor).toInt()
        val red = Color.red(color)
        val green = Color.green(color)
        val blue = Color.blue(color)
        return Color.argb(alpha, red, green, blue)
    }
}
package ir.cafebazaar.poolakey

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

// -------------------- Package Utilities --------------------

internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
    val packageManager = context.packageManager
    packageManager.getPackageInfo(packageName, 0)
} catch (ignored: Exception) {
    null
}

@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(packageInfo: PackageInfo): Long =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
    else packageInfo.versionCode.toLong()

internal fun getVersionName(context: Context, packageName: String): String? =
    getPackageInfo(context, packageName)?.versionName

internal fun isPackageInstalled(context: Context, packageName: String): Boolean =
    try {
        context.packageManager.getPackageInfo(packageName, 0)
        true
    } catch (e: Exception) {
        false
    }

internal fun isVersionAtLeast(context: Context, packageName: String, minVersionCode: Long): Boolean {
    val info = getPackageInfo(context, packageName)
    return info?.let { sdkAwareVersionCode(it) >= minVersionCode } ?: false
}

internal fun logPackageInfo(context: Context, packageName: String) {
    val info = getPackageInfo(context, packageName) ?: run {
        Log.w("Poolakey", "⚠️ Package $packageName not found or inaccessible.")
        return
    }
    val versionName = info.versionName ?: "N/A"
    val versionCode = sdkAwareVersionCode(info)
    Log.d("Poolakey", """
        📦 Package Info:
        - Package Name: $packageName
        - Version Name: $versionName
        - Version Code: $versionCode
        - First Install Time: ${info.firstInstallTime}
        - Last Update Time: ${info.lastUpdateTime}
    """.trimIndent())
}

internal fun getFirstInstallTime(context: Context, packageName: String): Long? =
    getPackageInfo(context, packageName)?.firstInstallTime

internal fun getLastUpdateTime(context: Context, packageName: String): Long? =
    getPackageInfo(context, packageName)?.lastUpdateTime

internal fun compareAppVersions(context: Context, packageName1: String, packageName2: String): Int {
    val info1 = getPackageInfo(context, packageName1)
    val info2 = getPackageInfo(context, packageName2)
    if (info1 == null || info2 == null) return 0
    return sdkAwareVersionCode(info1).compareTo(sdkAwareVersionCode(info2))
}

internal fun getPackageSummary(context: Context, packageName: String): String {
    val info = getPackageInfo(context, packageName)
    return if (info == null) "Package \"$packageName\" is not installed."
    else """
        📦 $packageName
        • Version: ${info.versionName ?: "Unknown"} (${sdkAwareVersionCode(info)})
        • Installed: ${info.firstInstallTime}
        • Updated: ${info.lastUpdateTime}
    """.trimIndent()
}

internal fun getAppAgeInDays(context: Context, packageName: String): Long? {
    val installTime = getFirstInstallTime(context, packageName) ?: return null
    return (System.currentTimeMillis() - installTime) / (1000 * 60 * 60 * 24)
}

internal fun needsUpdate(context: Context, packageName: String, remoteVersionCode: Long): Boolean {
    val currentVersion = getPackageInfo(context, packageName)?.let { sdkAwareVersionCode(it) } ?: return false
    return currentVersion < remoteVersionCode
}

internal fun getPackageUiSummary(context: Context, packageName: String): String {
    val info = getPackageInfo(context, packageName) ?: return "Package \"$packageName\" not installed."
    val age = getAppAgeInDays(context, packageName) ?: 0
    return "${info.packageName} v${info.versionName ?: "?"} (${sdkAwareVersionCode(info)}) - Installed $age days ago"
}

// -------------------- Connection State --------------------

sealed class ConnectionState {

    object Connected : ConnectionState()
    object FailedToConnect : ConnectionState()
    object Disconnected : ConnectionState()

    fun isConnected(): Boolean = this is Connected
    fun isDisconnected(): Boolean = this is Disconnected
    fun isFailed(): Boolean = this is FailedToConnect

    fun getDescription(): String = when (this) {
        Connected -> "✅ Connected to Bazaar billing service."
        FailedToConnect -> "❌ Failed to connect to Bazaar billing service."
        Disconnected -> "⚠️ Disconnected from Bazaar billing service."
    }

    fun toStatusCode(): String = when (this) {
        Connected -> "CONNECTED"
        FailedToConnect -> "FAILED"
        Disconnected -> "DISCONNECTED"
    }

    fun getEmoji(): String = when (this) {
        Connected -> "🟢"
        FailedToConnect -> "🔴"
        Disconnected -> "🟡"
    }

    fun logState(tag: String = "PoolakeyConnection") {
        Log.d(tag, "Connection State: ${toStatusCode()} - ${getDescription()}")
    }

    @ColorInt
    fun getStateColor(): Int = when (this) {
        Connected -> Color.parseColor("#4CAF50")
        FailedToConnect -> Color.parseColor("#F44336")
        Disconnected -> Color.parseColor("#FFC107")
    }

    fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"

    fun bindToStatusView(textView: TextView, context: Context) {
        textView.text = getUiLabel()
        val drawable = GradientDrawable().apply {
            shape = GradientDrawable.RECTANGLE
            cornerRadius = dpToPx(context, 16).toFloat()
            setColor(getStateColor())
        }
        textView.apply {
            setTextColor(Color.WHITE)
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 14f
            setPadding(dpToPx(context, 12), dpToPx(context, 6), dpToPx(context, 12), dpToPx(context, 6))
            background = drawable
        }
    }

    fun styleTextViewAsBadge(textView: TextView, context: Context) {
        val color = getStateColor()
        val drawable = GradientDrawable().apply {
            shape = GradientDrawable.RECTANGLE
            cornerRadius = dpToPx(context, 12).toFloat()
            setColor(adjustAlpha(color, 0.15f))
        }
        textView.apply {
            text = getUiLabel()
            setTextColor(color)
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 13f
            setPadding(dpToPx(context, 10), dpToPx(context, 4), dpToPx(context, 10), dpToPx(context, 4))
            background = drawable
        }
    }

    fun shouldAttemptReconnect(): Boolean = !isConnected()
    fun getNextSuggestedState(): ConnectionState = when (this) {
        Connected -> Disconnected
        else -> Connected
    }

    fun toNotificationMessage(): String = when (this) {
        Connected -> "🟢 Connection established with Bazaar."
        FailedToConnect -> "🔴 Unable to reach Bazaar servers. Please try again."
        Disconnected -> "🟡 Connection lost. Attempting to reconnect..."
    }

    fun getConnectionStabilityScore(): Int = when (this) {
        Connected -> 100
        FailedToConnect -> 25
        Disconnected -> 50
    }

    fun asLiveData(): LiveData<ConnectionState> = MutableLiveData<ConnectionState>().apply { value = this@ConnectionState }
    fun asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)

    private fun dpToPx(context: Context, dp: Int): Int {
        val scale = context.resources.displayMetrics.density
        return (dp * scale + 0.5f).toInt()
    }

    private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
        val alpha = (Color.alpha(color) * factor).toInt()
        return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
    }
}

// -------------------- Connection Interface --------------------

interface Connection {
    fun getState(): ConnectionState
    fun disconnect()
}

// -------------------- Additional ConnectionState Helpers --------------------

fun ConnectionState.toggleTestState(): ConnectionState = when (this) {
    ConnectionState.Connected -> ConnectionState.Disconnected
    ConnectionState.Disconnected -> ConnectionState.Connected
    ConnectionState.FailedToConnect -> ConnectionState.Connected
}

fun ConnectionState.getSeverityLevel(): Int = when (this) {
    ConnectionState.Connected -> 0
    ConnectionState.Disconnected -> 1
    ConnectionState.FailedToConnect -> 2
}

fun ConnectionState.updateTextViews(vararg textViews: TextView, context: Context) {
    textViews.forEach { bindToStatusView(it, context) }
}

fun ConnectionState.getUiLabelWithMessage(message: String?): String =
    if (message.isNullOrBlank()) getUiLabel() else "${getUiLabel()} - $message"

fun ConnectionState.getColoredCircleDrawable(): GradientDrawable = GradientDrawable().apply {
    shape = GradientDrawable.OVAL
    setColor(getStateColor())
    setSize(24, 24)
}

fun ConnectionState.asConnectedLiveData(): LiveData<Boolean> = MutableLiveData<Boolean>().apply { value = isConnected() }
fun ConnectionState.asStabilityStateFlow(): StateFlow<Int> = MutableStateFlow(getConnectionStabilityScore())
package ir.cafebazaar.poolakey

import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.view.View
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

// -------------------- Package Utilities --------------------

internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
    context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) {
    null
}

@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(packageInfo: PackageInfo): Long =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
    else packageInfo.versionCode.toLong()

internal fun getVersionName(context: Context, packageName: String): String? =
    getPackageInfo(context, packageName)?.versionName

internal fun isPackageInstalled(context: Context, packageName: String): Boolean =
    try { context.packageManager.getPackageInfo(packageName, 0); true } catch (_: Exception) { false }

internal fun isVersionAtLeast(context: Context, packageName: String, minVersionCode: Long): Boolean {
    val info = getPackageInfo(context, packageName)
    return info?.let { sdkAwareVersionCode(it) >= minVersionCode } ?: false
}

internal fun logPackageInfo(context: Context, packageName: String) {
    val info = getPackageInfo(context, packageName) ?: run {
        Log.w("Poolakey", "⚠️ Package $packageName not found or inaccessible.")
        return
    }
    Log.d("Poolakey", """
        📦 Package Info:
        - Package Name: $packageName
        - Version Name: ${info.versionName ?: "N/A"}
        - Version Code: ${sdkAwareVersionCode(info)}
        - First Install Time: ${info.firstInstallTime}
        - Last Update Time: ${info.lastUpdateTime}
    """.trimIndent())
}

internal fun getFirstInstallTime(context: Context, packageName: String): Long? =
    getPackageInfo(context, packageName)?.firstInstallTime

internal fun getLastUpdateTime(context: Context, packageName: String): Long? =
    getPackageInfo(context, packageName)?.lastUpdateTime

internal fun compareAppVersions(context: Context, packageName1: String, packageName2: String): Int {
    val info1 = getPackageInfo(context, packageName1)
    val info2 = getPackageInfo(context, packageName2)
    if (info1 == null || info2 == null) return 0
    return sdkAwareVersionCode(info1).compareTo(sdkAwareVersionCode(info2))
}

internal fun getPackageSummary(context: Context, packageName: String): String {
    val info = getPackageInfo(context, packageName) ?: return "Package \"$packageName\" is not installed."
    return """
        📦 $packageName
        • Version: ${info.versionName ?: "Unknown"} (${sdkAwareVersionCode(info)})
        • Installed: ${info.firstInstallTime}
        • Updated: ${info.lastUpdateTime}
    """.trimIndent()
}

internal fun getAppAgeInDays(context: Context, packageName: String): Long? {
    val installTime = getFirstInstallTime(context, packageName) ?: return null
    return (System.currentTimeMillis() - installTime) / (1000 * 60 * 60 * 24)
}

internal fun needsUpdate(context: Context, packageName: String, remoteVersionCode: Long): Boolean {
    val currentVersion = getPackageInfo(context, packageName)?.let { sdkAwareVersionCode(it) } ?: return false
    return currentVersion < remoteVersionCode
}

internal fun getPackageUiSummary(context: Context, packageName: String): String {
    val info = getPackageInfo(context, packageName) ?: return "Package \"$packageName\" not installed."
    val age = getAppAgeInDays(context, packageName) ?: 0
    return "${info.packageName} v${info.versionName ?: "?"} (${sdkAwareVersionCode(info)}) - Installed $age days ago"
}

// -------------------- Connection State --------------------

sealed class ConnectionState {

    object Connected : ConnectionState()
    object FailedToConnect : ConnectionState()
    object Disconnected : ConnectionState()

    fun isConnected(): Boolean = this is Connected
    fun isDisconnected(): Boolean = this is Disconnected
    fun isFailed(): Boolean = this is FailedToConnect

    fun getDescription(): String = when (this) {
        Connected -> "✅ Connected to Bazaar billing service."
        FailedToConnect -> "❌ Failed to connect to Bazaar billing service."
        Disconnected -> "⚠️ Disconnected from Bazaar billing service."
    }

    fun toStatusCode(): String = when (this) {
        Connected -> "CONNECTED"
        FailedToConnect -> "FAILED"
        Disconnected -> "DISCONNECTED"
    }

    fun getEmoji(): String = when (this) {
        Connected -> "🟢"
        FailedToConnect -> "🔴"
        Disconnected -> "🟡"
    }

    fun logState(tag: String = "PoolakeyConnection") {
        Log.d(tag, "Connection State: ${toStatusCode()} - ${getDescription()}")
    }

    @ColorInt
    fun getStateColor(): Int = when (this) {
        Connected -> Color.parseColor("#4CAF50")
        FailedToConnect -> Color.parseColor("#F44336")
        Disconnected -> Color.parseColor("#FFC107")
    }

    fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"

    fun bindToStatusView(textView: TextView, context: Context) {
        textView.text = getUiLabel()
        val drawable = GradientDrawable().apply {
            shape = GradientDrawable.RECTANGLE
            cornerRadius = dpToPx(context, 20).toFloat()
            setColor(getStateColor())
        }
        textView.apply {
            setTextColor(Color.WHITE)
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 14f
            setPadding(dpToPx(context, 16), dpToPx(context, 8), dpToPx(context, 16), dpToPx(context, 8))
            background = drawable
            elevation = 6f
        }
    }

    fun styleTextViewAsBadge(textView: TextView, context: Context) {
        val color = getStateColor()
        val drawable = GradientDrawable().apply {
            shape = GradientDrawable.RECTANGLE
            cornerRadius = dpToPx(context, 16).toFloat()
            setColor(adjustAlpha(color, 0.12f))
        }
        textView.apply {
            text = getUiLabel()
            setTextColor(color)
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 13f
            setPadding(dpToPx(context, 10), dpToPx(context, 4), dpToPx(context, 10), dpToPx(context, 4))
            background = drawable
            elevation = 4f
        }
    }

    fun shouldAttemptReconnect(): Boolean = !isConnected()
    fun getNextSuggestedState(): ConnectionState = when (this) {
        Connected -> Disconnected
        else -> Connected
    }

    fun toNotificationMessage(): String = when (this) {
        Connected -> "🟢 Connection established with Bazaar."
        FailedToConnect -> "🔴 Unable to reach Bazaar servers. Please try again."
        Disconnected -> "🟡 Connection lost. Attempting to reconnect..."
    }

    fun getConnectionStabilityScore(): Int = when (this) {
        Connected -> 100
        FailedToConnect -> 25
        Disconnected -> 50
    }

    fun asLiveData(): LiveData<ConnectionState> = MutableLiveData<ConnectionState>().apply { value = this@ConnectionState }
    fun asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)

    private fun dpToPx(context: Context, dp: Int): Int =
        (dp * context.resources.displayMetrics.density + 0.5f).toInt()

    private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
        val alpha = (Color.alpha(color) * factor).toInt()
        return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
    }
}

// -------------------- Connection Interface --------------------

interface Connection {
    fun getState(): ConnectionState
    fun disconnect()
}

// -------------------- Additional Helpers --------------------

fun ConnectionState.toggleTestState(): ConnectionState = when (this) {
    ConnectionState.Connected -> ConnectionState.Disconnected
    ConnectionState.Disconnected -> ConnectionState.Connected
    ConnectionState.FailedToConnect -> ConnectionState.Connected
}

fun ConnectionState.getSeverityLevel(): Int = when (this) {
    ConnectionState.Connected -> 0
    ConnectionState.Disconnected -> 1
    ConnectionState.FailedToConnect -> 2
}

fun ConnectionState.updateTextViews(vararg textViews: TextView, context: Context) {
    textViews.forEach { bindToStatusView(it, context) }
}

fun ConnectionState.getUiLabelWithMessage(message: String?): String {
    val baseLabel = getUiLabel()
    return if (message.isNullOrBlank()) baseLabel else "$baseLabel - $message"
}

fun ConnectionState.getColoredCircleDrawable(): GradientDrawable {
    return GradientDrawable().apply {
        shape = GradientDrawable.OVAL
        setColor(getStateColor())
        setSize(24, 24)
    }
}

fun ConnectionState.asConnectedLiveData(): LiveData<Boolean> =
    MutableLiveData<Boolean>().apply { value = isConnected() }

fun ConnectionState.asStabilityStateFlow(): StateFlow<Int> =
    MutableStateFlow(getConnectionStabilityScore())
// =======================================================
// ✅ Poolakey Complete Utilities + Extensions (Fixed + Enhanced)
// =======================================================

package ir.cafebazaar.poolakey

import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*

// =======================================================
// 📦 Package Utilities
// =======================================================

internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
    context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) { null }

@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(info: PackageInfo): Long =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()

internal fun getVersionName(context: Context, pkg: String): String? =
    getPackageInfo(context, pkg)?.versionName

internal fun getFirstInstallTime(context: Context, pkg: String): Long? =
    getPackageInfo(context, pkg)?.firstInstallTime

internal fun getLastUpdateTime(context: Context, pkg: String): Long? =
    getPackageInfo(context, pkg)?.lastUpdateTime

internal fun getAppAgeInDays(context: Context, pkg: String): Long? {
    val installTime = getFirstInstallTime(context, pkg) ?: return null
    return (System.currentTimeMillis() - installTime) / (1000L * 60 * 60 * 24)
}

internal fun getInstallDate(context: Context, pkg: String): String {
    val install = getFirstInstallTime(context, pkg) ?: return "Unknown"
    return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(install))
}

internal fun getUpdateDate(context: Context, pkg: String): String {
    val update = getLastUpdateTime(context, pkg) ?: return "Unknown"
    return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(update))
}

internal fun wasRecentlyUpdated(context: Context, pkg: String, days: Int = 7): Boolean {
    val last = getLastUpdateTime(context, pkg) ?: return false
    val since = (System.currentTimeMillis() - last) / (1000 * 60 * 60 * 24)
    return since <= days
}

// =======================================================
// 🔌 Connection State Management
// =======================================================

sealed class ConnectionState {
    object Connected : ConnectionState()
    object FailedToConnect : ConnectionState()
    object Disconnected : ConnectionState()

    fun isConnected() = this is Connected
    fun isDisconnected() = this is Disconnected
    fun isFailed() = this is FailedToConnect

    fun toStatusCode(): String = when (this) {
        Connected -> "CONNECTED"
        FailedToConnect -> "FAILED"
        Disconnected -> "DISCONNECTED"
    }

    fun getEmoji(): String = when (this) {
        Connected -> "🟢"
        FailedToConnect -> "🔴"
        Disconnected -> "🟡"
    }

    fun logState(tag: String = "PoolakeyConnection") {
        Log.d(tag, "State: ${toStatusCode()} (${getEmoji()})")
    }

    @ColorInt
    fun getStateColor(): Int = when (this) {
        Connected -> Color.parseColor("#4CAF50")
        FailedToConnect -> Color.parseColor("#F44336")
        Disconnected -> Color.parseColor("#FFC107")
    }

    fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"

    /** improved, gradient-based badge with shadow */
    fun bindToStatusView(view: TextView, ctx: Context) {
        val grad = GradientDrawable(
            GradientDrawable.Orientation.LEFT_RIGHT,
            intArrayOf(
                adjustBrightness(getStateColor(), 1.2f),
                adjustBrightness(getStateColor(), 0.8f)
            )
        ).apply {
            cornerRadius = dp(ctx, 24f)
        }

        view.apply {
            text = getUiLabel()
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 15f
            setTextColor(Color.WHITE)
            background = grad
            setPadding(dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt(), dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt())
            elevation = 8f
        }
    }

    private fun dp(ctx: Context, v: Float): Float = v * ctx.resources.displayMetrics.density

    private fun adjustBrightness(@ColorInt color: Int, factor: Float): Int {
        val r = (Color.red(color) * factor).coerceIn(0f, 255f)
        val g = (Color.green(color) * factor).coerceIn(0f, 255f)
        val b = (Color.blue(color) * factor).coerceIn(0f, 255f)
        return Color.rgb(r.toInt(), g.toInt(), b.toInt())
    }
}

// =======================================================
// 🔗 Connection Interface + Extensions
// =======================================================

interface Connection {
    fun getState(): ConnectionState
    fun disconnect()
}

class SafeConnection(private val ref: WeakReference<Connection>) : Connection {
    override fun getState(): ConnectionState = ref.get()?.getState() ?: ConnectionState.Disconnected
    override fun disconnect() { ref.get()?.disconnect() }

    fun reconnectIfNeeded(onReconnect: () -> Unit) {
        if (getState().isDisconnected() || getState().isFailed()) {
            Log.i("Poolakey", "Reconnecting …")
            onReconnect()
        }
    }
}

fun ConnectionState.asLiveData(): LiveData<ConnectionState> = MutableLiveData(this)
fun ConnectionState.asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)

fun combineConnectionStates(vararg s: ConnectionState): ConnectionState =
    when {
        s.any { it is ConnectionState.FailedToConnect } -> ConnectionState.FailedToConnect
        s.any { it is ConnectionState.Disconnected } -> ConnectionState.Disconnected
        else -> ConnectionState.Connected
    }

// =======================================================
// 🧵 Poolakey Thread Interface + Implementations
// =======================================================

internal interface PoolakeyThread<T> {
    fun execute(task: T)
    fun dispose()
}

internal class CoroutinePoolakeyThread : PoolakeyThread<Runnable> {
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    override fun execute(task: Runnable) { scope.launch { task.run() } }
    override fun dispose() { job.cancel() }
}

internal class MainThreadPoolakeyThread : PoolakeyThread<() -> Unit> {
    private val handler = Handler(Looper.getMainLooper())
    override fun execute(task: () -> Unit) { handler.post(task) }
    override fun dispose() { handler.removeCallbacksAndMessages(null) }
}

// =======================================================
// 🧠 Connection Monitoring Extensions
// =======================================================

suspend fun ConnectionState.monitorConnectionState(
    onConnected: suspend () -> Unit,
    onDisconnected: suspend () -> Unit,
    onFailed: suspend () -> Unit
) {
    when (this) {
        is ConnectionState.Connected -> onConnected()
        is ConnectionState.Disconnected -> onDisconnected()
        is ConnectionState.FailedToConnect -> onFailed()
    }
}

suspend fun retryUntilConnected(
    provider: suspend () -> ConnectionState,
    maxRetries: Int = 5,
    delayMs: Long = 2000L
): ConnectionState {
    repeat(maxRetries) { attempt ->
        val state = provider()
        if (state.isConnected()) return state
        Log.w("PoolakeyRetry", "Attempt ${attempt + 1}/$maxRetries failed – retrying …")
        delay(delayMs)
    }
    return ConnectionState.FailedToConnect
}
// =======================================================
// ✅ Poolakey Complete Utilities + Thread Implementations (Enhanced UI + Stability)
// =======================================================

package ir.cafebazaar.poolakey

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow

// =======================================================
// 📦 Package Utilities
// =======================================================

internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
    context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) { null }

@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(info: PackageInfo): Long =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()

internal fun getVersionName(context: Context, pkg: String): String? =
    getPackageInfo(context, pkg)?.versionName

internal fun getFirstInstallTime(context: Context, pkg: String): Long? =
    getPackageInfo(context, pkg)?.firstInstallTime

internal fun getLastUpdateTime(context: Context, pkg: String): Long? =
    getPackageInfo(context, pkg)?.lastUpdateTime

internal fun getAppAgeInDays(context: Context, pkg: String): Long? {
    val installTime = getFirstInstallTime(context, pkg) ?: return null
    return (System.currentTimeMillis() - installTime) / (1000L * 60 * 60 * 24)
}

internal fun getInstallDate(context: Context, pkg: String): String {
    val install = getFirstInstallTime(context, pkg) ?: return "Unknown"
    return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(install))
}

internal fun getUpdateDate(context: Context, pkg: String): String {
    val update = getLastUpdateTime(context, pkg) ?: return "Unknown"
    return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(Date(update))
}

internal fun wasRecentlyUpdated(context: Context, pkg: String, days: Int = 7): Boolean {
    val last = getLastUpdateTime(context, pkg) ?: return false
    val since = (System.currentTimeMillis() - last) / (1000 * 60 * 60 * 24)
    return since <= days
}

// =======================================================
// 🔌 Connection State Management
// =======================================================

sealed class ConnectionState {
    object Connected : ConnectionState()
    object FailedToConnect : ConnectionState()
    object Disconnected : ConnectionState()

    fun isConnected() = this is Connected
    fun isDisconnected() = this is Disconnected
    fun isFailed() = this is FailedToConnect

    fun toStatusCode(): String = when (this) {
        Connected -> "CONNECTED"
        FailedToConnect -> "FAILED"
        Disconnected -> "DISCONNECTED"
    }

    fun getEmoji(): String = when (this) {
        Connected -> "🟢"
        FailedToConnect -> "🔴"
        Disconnected -> "🟡"
    }

    fun logState(tag: String = "PoolakeyConnection") {
        Log.d(tag, "State: ${toStatusCode()} (${getEmoji()})")
    }

    @ColorInt
    fun getStateColor(): Int = when (this) {
        Connected -> Color.parseColor("#4CAF50")   // Green
        FailedToConnect -> Color.parseColor("#E53935") // Red (Material shade)
        Disconnected -> Color.parseColor("#FFC107") // Amber
    }

    fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"

    // =======================================================
    // ✨ Improved UI Binding (Animated, Rounded, Adaptive)
    // =======================================================
    fun bindToStatusView(view: TextView, ctx: Context, animate: Boolean = true) {
        val newColor = getStateColor()
        val currentBg = (view.background as? GradientDrawable)?.colors?.firstOrNull() ?: Color.GRAY

        if (animate) {
            val anim = ValueAnimator.ofObject(ArgbEvaluator(), currentBg, newColor)
            anim.duration = 450
            anim.addUpdateListener { valueAnimator ->
                val color = valueAnimator.animatedValue as Int
                val grad = GradientDrawable(
                    GradientDrawable.Orientation.LEFT_RIGHT,
                    intArrayOf(
                        adjustBrightness(color, 1.15f),
                        adjustBrightness(color, 0.85f)
                    )
                ).apply {
                    cornerRadius = dp(ctx, 26f)
                }
                view.background = grad
            }
            anim.start()
        } else {
            val grad = GradientDrawable(
                GradientDrawable.Orientation.LEFT_RIGHT,
                intArrayOf(
                    adjustBrightness(newColor, 1.15f),
                    adjustBrightness(newColor, 0.85f)
                )
            ).apply {
                cornerRadius = dp(ctx, 26f)
            }
            view.background = grad
        }

        view.apply {
            text = getUiLabel()
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 15.5f
            setTextColor(Color.WHITE)
            setPadding(dp(ctx, 22f).toInt(), dp(ctx, 12f).toInt(), dp(ctx, 22f).toInt(), dp(ctx, 12f).toInt())
            elevation = 10f
        }
    }

    private fun dp(ctx: Context, v: Float): Float = v * ctx.resources.displayMetrics.density

    private fun adjustBrightness(@ColorInt color: Int, factor: Float): Int {
        val r = (Color.red(color) * factor).coerceIn(0f, 255f)
        val g = (Color.green(color) * factor).coerceIn(0f, 255f)
        val b = (Color.blue(color) * factor).coerceIn(0f, 255f)
        return Color.rgb(r.toInt(), g.toInt(), b.toInt())
    }

    fun formatMessage(): String {
        val ts = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
        return "[$ts] ${getUiLabel()}"
    }

    fun toIntCode(): Int = when (this) {
        Connected -> 1
        FailedToConnect -> -1
        Disconnected -> 0
    }
}

// =======================================================
// 🔗 Connection Interface + Extensions
// =======================================================

interface Connection {
    fun getState(): ConnectionState
    fun disconnect()
}

class SafeConnection(private val ref: WeakReference<Connection>) : Connection {
    override fun getState(): ConnectionState = ref.get()?.getState() ?: ConnectionState.Disconnected
    override fun disconnect() { ref.get()?.disconnect() }

    fun reconnectIfNeeded(onReconnect: () -> Unit) {
        if (getState().isDisconnected() || getState().isFailed()) {
            Log.i("Poolakey", "Reconnecting …")
            onReconnect()
        }
    }

    fun monitorContinuously(intervalMs: Long = 5000L, onStateChange: (ConnectionState) -> Unit) {
        val weakHandler = WeakReference(Handler(Looper.getMainLooper()))
        val runnable = object : Runnable {
            override fun run() {
                val state = getState()
                onStateChange(state)
                weakHandler.get()?.postDelayed(this, intervalMs)
            }
        }
        weakHandler.get()?.post(runnable)
    }
}

fun ConnectionState.asLiveData(): LiveData<ConnectionState> = MutableLiveData(this)
fun ConnectionState.asStateFlow(): StateFlow<ConnectionState> = MutableStateFlow(this)

fun combineConnectionStates(vararg s: ConnectionState): ConnectionState =
    when {
        s.any { it is ConnectionState.FailedToConnect } -> ConnectionState.FailedToConnect
        s.any { it is ConnectionState.Disconnected } -> ConnectionState.Disconnected
        else -> ConnectionState.Connected
    }

// =======================================================
// 🧵 Poolakey Thread Interface + Implementations
// =======================================================

internal interface PoolakeyThread<T> {
    fun execute(task: T)
    fun dispose()
}

internal class CoroutinePoolakeyThread : PoolakeyThread<Runnable> {
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    override fun execute(task: Runnable) { scope.launch { task.run() } }
    override fun dispose() { job.cancel() }
}

// =======================================================
// 🧵 MainThread Handler Implementation (Improved Safety)
// =======================================================

internal class MainThread : Handler(Looper.getMainLooper()), PoolakeyThread<() -> Unit> {
    override fun handleMessage(message: Message) {
        (message.obj as? Function0<*>)?.invoke()
            ?: Log.e("PoolakeyMainThread", "Invalid message received")
    }

    override fun execute(task: () -> Unit) {
        val message = obtainMessage().apply { obj = task }
        sendMessage(message)
    }

    override fun dispose() {
        removeCallbacksAndMessages(null)
    }
}

// =======================================================
// 🧠 Connection Monitoring + Retry Utilities
// =======================================================

suspend fun ConnectionState.monitorConnectionState(
    onConnected: suspend () -> Unit,
    onDisconnected: suspend () -> Unit,
    onFailed: suspend () -> Unit
) {
    when (this) {
        is ConnectionState.Connected -> onConnected()
        is ConnectionState.Disconnected -> onDisconnected()
        is ConnectionState.FailedToConnect -> onFailed()
    }
}

suspend fun retryUntilConnected(
    provider: suspend () -> ConnectionState,
    maxRetries: Int = 5,
    delayMs: Long = 2000L
): ConnectionState {
    repeat(maxRetries) { attempt ->
        val state = provider()
        if (state.isConnected()) return state
        Log.w("PoolakeyRetry", "Attempt ${attempt + 1}/$maxRetries failed – retrying …")
        delay(delayMs)
    }
    return ConnectionState.FailedToConnect
}

// =======================================================
// 🧮 Advanced Diagnostics + Backoff
// =======================================================

suspend fun measureConnectionDuration(action: suspend () -> ConnectionState): Pair<ConnectionState, Long> {
    val start = System.currentTimeMillis()
    val result = action()
    val duration = System.currentTimeMillis() - start
    Log.i("PoolakeyMetrics", "Connection took ${duration}ms → ${result.toStatusCode()}")
    return result to duration
}

fun ConnectionState.toMap(): Map<String, Any> = mapOf(
    "status" to toStatusCode(),
    "emoji" to getEmoji(),
    "code" to toIntCode(),
    "timestamp" to System.currentTimeMillis()
)

suspend fun exponentialRetry(
    action: suspend () -> ConnectionState,
    maxAttempts: Int = 5,
    baseDelay: Long = 1000L
): ConnectionState {
    repeat(maxAttempts) { attempt ->
        val state = action()
        if (state.isConnected()) return state
        val delayTime = baseDelay * (2.0.pow(attempt)).toLong()
        Log.w("PoolakeyBackoff", "Attempt ${attempt + 1}/$maxAttempts failed, retrying in ${delayTime}ms")
        delay(delayTime)
    }
    return ConnectionState.FailedToConnect
}
// =======================================================
// ✅ Poolakey Complete Utilities + Thread Implementations (Enhanced Edition)
// =======================================================

package ir.cafebazaar.poolakey

import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.pm.PackageInfo
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.*
import android.util.Log
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow

// =======================================================
// 📦 Package Utilities
// =======================================================

internal fun getPackageInfo(context: Context, packageName: String): PackageInfo? = try {
    context.packageManager.getPackageInfo(packageName, 0)
} catch (_: Exception) {
    null
}

@Suppress("DEPRECATION")
internal fun sdkAwareVersionCode(info: PackageInfo): Long =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()

internal fun getVersionName(context: Context, pkg: String): String? =
    getPackageInfo(context, pkg)?.versionName

internal fun getFirstInstallTime(context: Context, pkg: String): Long? =
    getPackageInfo(context, pkg)?.firstInstallTime

internal fun getLastUpdateTime(context: Context, pkg: String): Long? =
    getPackageInfo(context, pkg)?.lastUpdateTime

internal fun getAppAgeInDays(context: Context, pkg: String): Long? {
    val installTime = getFirstInstallTime(context, pkg) ?: return null
    return (System.currentTimeMillis() - installTime) / (1000L * 60 * 60 * 24)
}

// =======================================================
// 🔌 Connection State Management
// =======================================================

sealed class ConnectionState {
    object Connected : ConnectionState()
    object FailedToConnect : ConnectionState()
    object Disconnected : ConnectionState()

    fun isConnected() = this is Connected
    fun isDisconnected() = this is Disconnected
    fun isFailed() = this is FailedToConnect

    fun toStatusCode(): String = when (this) {
        Connected -> "CONNECTED"
        FailedToConnect -> "FAILED"
        Disconnected -> "DISCONNECTED"
    }

    fun getEmoji(): String = when (this) {
        Connected -> "🟢"
        FailedToConnect -> "🔴"
        Disconnected -> "🟡"
    }

    fun logState(tag: String = "PoolakeyConnection") {
        Log.d(tag, "State: ${toStatusCode()} (${getEmoji()})")
    }

    @ColorInt
    fun getStateColor(): Int = when (this) {
        Connected -> Color.parseColor("#4CAF50")
        FailedToConnect -> Color.parseColor("#F44336")
        Disconnected -> Color.parseColor("#FFC107")
    }

    fun getUiLabel(): String = "${getEmoji()} ${toStatusCode()}"

    /**
     * 🌈 Visually improved UI for connection status
     */
    @mainthread
    fun bindToStatusView(view: TextView, ctx: Context) {
        val color = getStateColor()
        val startGradient = adjustBrightness(color, 1.25f)
        val endGradient = adjustBrightness(color, 0.85f)

        val grad = GradientDrawable(
            GradientDrawable.Orientation.LEFT_RIGHT,
            intArrayOf(startGradient, endGradient)
        ).apply {
            cornerRadius = dp(ctx, 24f)
        }

        // Animate color transition for smoother feedback
        val prevBackground = (view.background as? GradientDrawable)?.colors?.firstOrNull() ?: color
        val colorAnim = ValueAnimator.ofObject(ArgbEvaluator(), prevBackground, color).apply {
            duration = 400
            interpolator = AccelerateDecelerateInterpolator()
            addUpdateListener {
                view.setBackgroundColor(it.animatedValue as Int)
            }
        }

        colorAnim.start()

        view.apply {
            text = getUiLabel()
            setTypeface(Typeface.DEFAULT_BOLD)
            textSize = 15f
            setTextColor(Color.WHITE)
            background = grad
            setPadding(dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt(), dp(ctx, 20f).toInt(), dp(ctx, 10f).toInt())
            elevation = 8f
            alpha = 0f
            animate().alpha(1f).setDuration(300).start()
        }
    }

    private fun dp(ctx: Context, v: Float): Float = v * ctx.resources.displayMetrics.density

    private fun adjustBrightness(@ColorInt color: Int, factor: Float): Int {
        val r = (Color.red(color) * factor).coerceIn(0f, 255f)
        val g = (Color.green(color) * factor).coerceIn(0f, 255f)
        val b = (Color.blue(color) * factor).coerceIn(0f, 255f)
        return Color.rgb(r.toInt(), g.toInt(), b.toInt())
    }

    fun formatMessage(): String {
        val ts = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
        return "[$ts] ${getUiLabel()}"
    }

    fun toIntCode(): Int = when (this) {
        Connected -> 1
        FailedToConnect -> -1
        Disconnected -> 0
    }
}

// =======================================================
// 🔗 Connection Interface + Extensions
// =======================================================

interface Connection {
    fun getState(): ConnectionState
    fun disconnect()
}

class SafeConnection(private val ref: WeakReference<Connection>) : Connection {
    override fun getState(): ConnectionState = ref.get()?.getState() ?: ConnectionState.Disconnected
    override fun disconnect() {
        ref.get()?.disconnect()
    }

    fun reconnectIfNeeded(onReconnect: () -> Unit) {
        if (getState().isDisconnected() || getState().isFailed()) {
            Log.i("Poolakey", "Reconnecting …")
            onReconnect()
        }
    }
}

// =======================================================
// 🧵 Poolakey Thread Interface + Implementations
// =======================================================

internal interface PoolakeyThread<T> {
    fun execute(task: T)
    fun dispose()
}

internal class CoroutinePoolakeyThread : PoolakeyThread<Runnable> {
    private val job = SupervisorJob()
    private val scope = CoroutineScope(Dispatchers.IO + job)
    override fun execute(task: Runnable) {
        scope.launch { task.run() }
    }

    override fun dispose() {
        job.cancel()
    }
}

internal class MainThread : Handler(Looper.getMainLooper()), PoolakeyThread<() -> Unit> {
    override fun handleMessage(message: Message) {
        super.handleMessage(message)
        (message.obj as? Function0<*>)?.invoke()
            ?: Log.e("MainThread", "Message is corrupted!")
    }

    override fun execute(task: () -> Unit) {
        val msg = Message.obtain().apply { obj = task }
        sendMessage(msg)
    }

    override fun dispose() {
        removeCallbacksAndMessages(null)
    }
}

internal class BackgroundThread : HandlerThread("PoolakeyThread"), PoolakeyThread<Runnable> {
    private lateinit var threadHandler: Handler

    init {
        start()
        threadHandler = Handler(looper)
    }

    override fun execute(task: Runnable) {
        if (::threadHandler.isInitialized) {
            threadHandler.post(task)
        }
    }

    override fun dispose() {
        threadHandler.removeCallbacksAndMessages(null)
        quitSafely()
    }
}

// =======================================================
// 🧠 Advanced Utilities & UI Diagnostics
// =======================================================

fun ConnectionState.renderTo(view: TextView) {
    val formatted = "${getEmoji()} ${toStatusCode()} • ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())}"
    view.text = formatted
    view.setTextColor(getStateColor())
    view.animate().alpha(1f).setDuration(250).start()
}

fun ConnectionState.toJsonString(): String =
    """{"status":"${toStatusCode()}","emoji":"${getEmoji()}","code":${toIntCode()},"timestamp":${System.currentTimeMillis()}}"""

fun collectConnectionDiagnostics(connection: Connection): Map<String, Any> = mapOf(
    "state" to connection.getState().toStatusCode(),
    "connected" to connection.getState().isConnected(),
    "device" to Build.MODEL,
    "sdk" to Build.VERSION.SDK_INT,
    "time" to System.currentTimeMillis()
)
package ir.cafebazaar.poolakey.security

import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.util.Base64
import android.util.Log
import android.widget.TextView
import java.lang.IllegalArgumentException
import java.nio.charset.StandardCharsets
import java.security.*
import java.security.spec.InvalidKeySpecException
import java.security.spec.X509EncodedKeySpec
import javax.crypto.Cipher

// =======================================================
// 🔐 Secure Purchase Verifier (Ultimate Extended Version)
// =======================================================

internal class PurchaseVerifier {

    @throws(
        NoSuchAlgorithmException::class,
        InvalidKeySpecException::class,
        InvalidKeyException::class,
        SignatureException::class,
        IllegalArgumentException::class
    )
    fun verifyPurchase(base64PublicKey: String, signedData: String, signature: String): Boolean {
        val key = generatePublicKey(base64PublicKey)
        return verify(key, signedData, signature)
    }

    @throws(
        NoSuchAlgorithmException::class,
        InvalidKeySpecException::class,
        IllegalArgumentException::class
    )
    private fun generatePublicKey(encodedPublicKey: String): PublicKey {
        // normalize input to remove spaces/newlines that sometimes appear in keys
        val normalized = normalizeBase64Input(encodedPublicKey)
        val decodedKey = Base64.decode(normalized, Base64.DEFAULT)
        val keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
        return keyFactory.generatePublic(X509EncodedKeySpec(decodedKey))
    }

    @throws(NoSuchAlgorithmException::class, InvalidKeyException::class, SignatureException::class)
    private fun verify(publicKey: PublicKey, signedData: String, signature: String): Boolean {
        val signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM)
        signatureAlgorithm.initVerify(publicKey)
        signatureAlgorithm.update(signedData.toByteArray(StandardCharsets.UTF_8))
        return signatureAlgorithm.verify(Base64.decode(signature, Base64.DEFAULT))
    }

    companion object {
        private const val KEY_FACTORY_ALGORITHM = "RSA"
        private const val SIGNATURE_ALGORITHM = "SHA1withRSA"

        // =======================================================
        // 🆕 Extended & Advanced Utility Functions
        // =======================================================

        /**
         * Enhanced verification using SHA256 fallback.
         */
        fun verifySecure(
            base64PublicKey: String,
            signedData: String,
            signature: String
        ): VerificationResult {
            return try {
                val key = generateKey(base64PublicKey)
                val verifiedSHA1 = tryVerify(key, signedData, signature, "SHA1withRSA")
                val verifiedSHA256 = verifiedSHA1 || tryVerify(key, signedData, signature, "SHA256withRSA")

                when {
                    verifiedSHA256 -> VerificationResult(success = true, algorithm = "SHA256withRSA")
                    verifiedSHA1 -> VerificationResult(success = true, algorithm = "SHA1withRSA")
                    else -> VerificationResult(success = false, errorMessage = "Signature verification failed.")
                }
            } catch (e: Exception) {
                Log.e("PurchaseVerifier", "Verification error: ${e.message}")
                VerificationResult(success = false, errorMessage = e.localizedMessage ?: "Unknown error")
            }
        }

        private fun generateKey(encodedPublicKey: String): PublicKey {
            val normalized = normalizeBase64Input(encodedPublicKey)
            if (!isBase64Valid(normalized)) {
                throw IllegalArgumentException("Invalid Base64 key format")
            }
            val decodedKey = Base64.decode(normalized, Base64.DEFAULT)
            val factory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
            return factory.generatePublic(X509EncodedKeySpec(decodedKey))
        }

        private fun tryVerify(
            publicKey: PublicKey,
            signedData: String,
            signature: String,
            algorithm: String
        ): Boolean {
            return try {
                val verifier = Signature.getInstance(algorithm)
                verifier.initVerify(publicKey)
                verifier.update(signedData.toByteArray(StandardCharsets.UTF_8))
                verifier.verify(Base64.decode(signature, Base64.DEFAULT))
            } catch (_: Exception) {
                false
            }
        }

        fun isBase64Valid(data: String): Boolean {
            return try {
                Base64.decode(data, Base64.DEFAULT)
                true
            } catch (_: IllegalArgumentException) {
                false
            }
        }

        fun generateDiagnosticReport(
            publicKey: String,
            signedData: String,
            signature: String
        ): Map<String, Any> {
            val normalized = normalizeBase64Input(publicKey)
            val base64Valid = isBase64Valid(normalized)
            val report = mutableMapOf<String, Any>(
                "base64Valid" to base64Valid,
                "signedDataLength" to signedData.length,
                "signatureLength" to signature.length,
                "timestamp" to System.currentTimeMillis()
            )
            if (!base64Valid) report["error"] = "Invalid Base64 key format"
            return report
        }

        data class VerificationResult(
            val success: Boolean,
            val algorithm: String? = null,
            val errorMessage: String? = null,
            val timestamp: Long = System.currentTimeMillis()
        ) {
            fun toPrettyString(): String {
                return if (success) {
                    "✅ Purchase verified successfully using $algorithm at $timestamp"
                } else {
                    "❌ Verification failed: ${errorMessage ?: "Unknown error"}"
                }
            }

            /** UI label — short */
            fun uiLabel(): String = if (success) "Verified" else "Not Verified"

            /** Suggested color for UI badge */
            fun uiColor(): Int = if (success) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
        }

        fun encodeToBase64(data: ByteArray): String =
            Base64.encodeToString(data, Base64.NO_WRAP)

        fun decodeFromBase64(data: String): ByteArray? =
            try { Base64.decode(normalizeBase64Input(data), Base64.DEFAULT) } catch (_: Exception) { null }

        fun isKeyValid(encodedKey: String): Boolean {
            return try {
                val key = generateKey(encodedKey)
                key.algorithm == KEY_FACTORY_ALGORITHM
            } catch (_: Exception) {
                false
            }
        }

        // =======================================================
        // 🧩 NEW FUNCTIONS FOR SECURITY AND DIAGNOSTICS
        // =======================================================

        /**
         * Returns a SHA-256 fingerprint of a public key for easy auditing/logging.
         */
        fun getPublicKeyFingerprint(encodedPublicKey: String): String? {
            return try {
                val keyBytes = Base64.decode(normalizeBase64Input(encodedPublicKey), Base64.DEFAULT)
                val digest = MessageDigest.getInstance("SHA-256")
                val hash = digest.digest(keyBytes)
                hash.joinToString(":") { "%02X".format(it) }
            } catch (e: Exception) {
                Log.e("PurchaseVerifier", "Fingerprint error: ${e.message}")
                null
            }
        }

        /**
         * Encrypts data using the provided RSA public key.
         * This can be useful for securely transmitting app data.
         */
        fun encryptWithPublicKey(data: String, base64PublicKey: String): String? {
            return try {
                val publicKey = generateKey(base64PublicKey)
                val cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
                cipher.init(Cipher.ENCRYPT_MODE, publicKey)
                val encryptedBytes = cipher.doFinal(data.toByteArray(StandardCharsets.UTF_8))
                encodeToBase64(encryptedBytes)
            } catch (e: Exception) {
                Log.e("PurchaseVerifier", "Encryption failed: ${e.message}")
                null
            }
        }

        /**
         * Verifies if two signatures are equivalent across different algorithms (SHA1 vs SHA256).
         * Useful for cross-version signature migration.
         */
        fun compareSignatureAlgorithms(
            base64PublicKey: String,
            signedData: String,
            signatureSHA1: String,
            signatureSHA256: String
        ): Boolean {
            return try {
                val key = generateKey(base64PublicKey)
                val sha1Valid = tryVerify(key, signedData, signatureSHA1, "SHA1withRSA")
                val sha256Valid = tryVerify(key, signedData, signatureSHA256, "SHA256withRSA")
                sha1Valid && sha256Valid
            } catch (_: Exception) {
                false
            }
        }

        /**
         * Performs a complete diagnostic test of a purchase verification flow.
         */
        fun runFullDiagnostic(
            base64PublicKey: String,
            signedData: String,
            signature: String
        ): String {
            val report = generateDiagnosticReport(base64PublicKey, signedData, signature)
            val fingerprint = getPublicKeyFingerprint(base64PublicKey) ?: "Unavailable"
            val verification = verifySecure(base64PublicKey, signedData, signature)
            return buildString {
                appendLine("🔍 Poolakey Verification Diagnostic")
                appendLine("===================================")
                appendLine("Public Key Fingerprint: $fingerprint")
                appendLine("Base64 Valid: ${report["base64Valid"]}")
                appendLine("Signed Data Length: ${report["signedDataLength"]}")
                appendLine("Signature Length: ${report["signatureLength"]}")
                appendLine("Verification Result: ${verification.toPrettyString()}")
                appendLine("Timestamp: ${report["timestamp"]}")
                if (report.containsKey("error")) {
                    appendLine("⚠️ Error: ${report["error"]}")
                }
            }
        }

        /**
         * Sanitizes and normalizes Base64 input by removing unwanted whitespace or line breaks.
         */
        fun normalizeBase64Input(data: String): String {
            return data.replace("\\s".toRegex(), "")
        }

        /**
         * Returns a simplified verification boolean with internal error safety.
         */
        fun quickVerify(base64PublicKey: String, signedData: String, signature: String): Boolean {
            return verifySecure(base64PublicKey, signedData, signature).success
        }
    }
}

// =======================================================
// 🖼 UI Helpers (visual improvements for verification output)
// =======================================================

/**
 * Apply a visually appealing badge to a TextView to show verification result.
 * - sets text to `result.uiLabel()` (e.g., "Verified" / "Not Verified")
 * - sets a rounded gradient background and appropriate text color
 * - optional small label (algorithm or message) appended
 */
fun styleVerificationBadge(textView: TextView, result: PurchaseVerifier.Companion.VerificationResult, context: Context, smallLabel: String? = null) {
    val bgColor = if (result.success) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
    val start = adjustAlpha(bgColor, 1.12f)
    val end = adjustAlpha(bgColor, 0.90f)

    val grad = GradientDrawable(
        GradientDrawable.Orientation.LEFT_RIGHT,
        intArrayOf(start, end)
    ).apply {
        cornerRadius = dp(context, 18f)
    }

    textView.apply {
        text = if (smallLabel.isNullOrBlank()) result.uiLabel() else "${result.uiLabel()} • $smallLabel"
        setTextColor(Color.WHITE)
        setTypeface(Typeface.DEFAULT_BOLD)
        textSize = 14f
        setPadding(dp(context, 14f).toInt(), dp(context, 8f).toInt(), dp(context, 14f).toInt(), dp(context, 8f).toInt())
        background = grad
        elevation = 6f
    }
}

/** small helpers for UI */
private fun dp(context: Context, v: Float): Float = v * context.resources.displayMetrics.density

private fun adjustAlpha(@ColorInt color: Int, factor: Float): Int {
    val r = (Color.red(color) * factor).coerceIn(0f, 255f).toInt()
    val g = (Color.green(color) * factor).coerceIn(0f, 255f).toInt()
    val b = (Color.blue(color) * factor).coerceIn(0f, 255f).toInt()
    val a = Color.alpha(color)
    return Color.argb(a, r, g, b)
}
package ir.cafebazaar.poolakey.security

import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.util.Log
import android.view.View
import android.widget.TextView
import android.widget.Toast
import ir.cafebazaar.poolakey.BuildConfig
import ir.cafebazaar.poolakey.constant.Const.BAZAAR_PACKAGE_NAME
import ir.cafebazaar.poolakey.getPackageInfo
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.PublicKey
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*

/**
 * Security utilities for Poolakey — verifies Bazaar installation & certificates,
 * provides diagnostics and some lightweight UI helpers for displaying reports.
 *
 * All functions are `internal` so they remain library-internal.
 */
internal object Security {

    private const val TAG = "PoolakeySecurity"

    // =======================================================
    // ✅ EXISTING CORE VERIFICATION
    // =======================================================

    /**
     * Verifies Bazaar is installed and its certificate public-key hex matches
     * the expected hash from BuildConfig.BAZAAR_HASH.
     */
    fun verifyBazaarIsInstalled(context: Context): Boolean {
        val packageInfo = getPackageInfo(context, BAZAAR_PACKAGE_NAME)
            ?: return false.also { Log.w(TAG, "Bazaar not installed.") }

        val signatures = getSignaturesSafe(context, BAZAAR_PACKAGE_NAME)
        if (signatures.isEmpty()) {
            Log.e(TAG, "No signatures found for Bazaar package.")
            return false
        }

        for (signature in signatures) {
            val certificate = signatureToX509(signature) ?: continue
            val publicKey: PublicKey = certificate.publicKey
            val certificateHex = byte2HexFormatted(publicKey.encoded)
            if (BuildConfig.BAZAAR_HASH == certificateHex) {
                Log.i(TAG, "✅ Bazaar verification successful.")
                return true
            }
        }

        Log.w(TAG, "❌ Bazaar signature mismatch detected.")
        return false
    }

    // =======================================================
    // 🆕 ADDITIONAL SECURITY & DIAGNOSTIC FUNCTIONS
    // =======================================================

    /**
     * Returns Bazaar app version info (for logging or UI display).
     */
    fun getBazaarVersionInfo(context: Context): String? {
        val packageInfo = getPackageInfo(context, BAZAAR_PACKAGE_NAME) ?: return null
        // Use longVersionCode on newer SDKs if available, else versionCode
        val versionCode = try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
            else packageInfo.versionCode.toLong()
        } catch (e: Exception) {
            -1L
        }
        return "Bazaar ${packageInfo.versionName} (code: $versionCode)"
    }

    /**
     * Verifies Bazaar's certificate SHA-256 fingerprint equals expectedFingerprint.
     * Fingerprint format should be hex pairs separated by ':' (uppercase or lowercase accepted).
     */
    fun verifyBazaarCertificateSHA256(context: Context, expectedFingerprint: String): Boolean {
        val signatures = getSignaturesSafe(context, BAZAAR_PACKAGE_NAME)
        if (signatures.isEmpty()) {
            Log.e(TAG, "No signatures available for SHA-256 check.")
            return false
        }
        val normalizedExpected = expectedFingerprint.replace("\\s".toRegex(), "").uppercase(Locale.US)
        for (signature in signatures) {
            val cert = signatureToX509(signature) ?: continue
            val fingerprint = getCertificateSHA256Fingerprint(cert).replace(":", "")
            if (fingerprint.equals(normalizedExpected, ignoreCase = true) ||
                getCertificateSHA256Fingerprint(cert).equals(expectedFingerprint, ignoreCase = true)
            ) {
                Log.i(TAG, "✅ Bazaar certificate SHA-256 verified successfully.")
                return true
            }
        }
        Log.e(TAG, "❌ Bazaar certificate SHA-256 verification failed.")
        return false
    }

    /**
     * Extracts SHA-256 fingerprint of a certificate, formatted as 'AA:BB:CC...'.
     */
    private fun getCertificateSHA256Fingerprint(certificate: X509Certificate): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest(certificate.encoded)
        return hash.joinToString(":") { "%02X".format(it) }
    }

    /**
     * Generates a short security report about Bazaar installation & signatures.
     */
    fun generateSecurityReport(context: Context): String {
        val packageInfo = getPackageInfo(context, BAZAAR_PACKAGE_NAME)
        val installed = packageInfo != null
        val version = packageInfo?.versionName ?: "N/A"
        val signatures = if (installed) getSignaturesSafe(context, BAZAAR_PACKAGE_NAME).size else 0

        return buildString {
            appendLine("🔍 Poolakey Security Report")
            appendLine("===================================")
            appendLine("📦 Bazaar Installed: $installed")
            appendLine("🏷️  Version: $version")
            appendLine("🔏 Signature Count: $signatures")
            appendLine("🕒 Generated: ${Date()}")
            appendLine("===================================")
        }
    }

    /**
     * Safe wrapper to get signatures — returns empty array instead of throwing.
     */
    @Suppress("DEPRECATION")
    @SuppressLint("PackageManagerGetSignatures")
    private fun getSignaturesSafe(context: Context, packageName: String): Array<Signature> {
        val packageManager = context.packageManager
        return try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                val packageInfo = packageManager.getPackageInfo(
                    packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
                )
                packageInfo.signingInfo.apkContentsSigners
            } else {
                val packageInfo = packageManager.getPackageInfo(
                    packageName,
                    PackageManager.GET_SIGNATURES
                )
                packageInfo.signatures
            }
        } catch (e: Exception) {
            Log.w(TAG, "getSignaturesSafe: unable to fetch signatures for $packageName: ${e.message}")
            emptyArray()
        }
    }

    /**
     * Converts a Signature to X509Certificate, or null on error.
     */
    private fun signatureToX509(signature: Signature): X509Certificate? {
        return try {
            val input: InputStream = ByteArrayInputStream(signature.toByteArray())
            val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X509")
            certificateFactory.generateCertificate(input) as X509Certificate
        } catch (e: Exception) {
            Log.w(TAG, "signatureToX509: failed to convert signature to certificate: ${e.message}")
            null
        }
    }

    /**
     * Converts a byte array into a formatted hex string (used for certificate comparison).
     * Uses unsigned byte handling to avoid negative hex values.
     */
    private fun byte2HexFormatted(array: ByteArray): String {
        val stringBuilder = StringBuilder(array.size * 3) // include ':' separators
        for (index in array.indices) {
            val unsigned = array[index].toInt() and 0xFF
            val suggestedHex = Integer.toHexString(unsigned)
            if (suggestedHex.length == 1) {
                stringBuilder.append('0')
            }
            stringBuilder.append(suggestedHex.uppercase(Locale.US))
            if (index < array.size - 1) {
                stringBuilder.append(':')
            }
        }
        return stringBuilder.toString()
    }

    /**
     * Prints the security report to logcat.
     */
    fun printSecuritySummary(context: Context) {
        val report = generateSecurityReport(context)
        Log.i(TAG, report)
    }

    // =======================================================
    // 🧩 NEWLY ADDED UTILITY FUNCTIONS
    // =======================================================

    /**
     * Detects common indicators of a rooted device by checking for known su locations.
     * Note: This is heuristic and not foolproof.
     */
    fun isDeviceRooted(): Boolean {
        val dangerousPaths = arrayOf(
            "/system/app/Superuser.apk",
            "/sbin/su",
            "/system/bin/su",
            "/system/xbin/su",
            "/data/local/xbin/su",
            "/data/local/bin/su",
            "/system/sd/xbin/su",
            "/system/bin/failsafe/su",
            "/data/local/su"
        )
        return dangerousPaths.any { path -> java.io.File(path).exists() }
    }

    /**
     * Returns true if the current app is debuggable.
     */
    fun isAppDebuggable(context: Context): Boolean {
        return (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
    }

    /**
     * Returns app's own SHA-256 certificate fingerprint (first signature) or null.
     */
    fun getOwnAppSignatureFingerprint(context: Context): String? {
        val signatures = getSignaturesSafe(context, context.packageName)
        if (signatures.isEmpty()) return null
        val cert = signatureToX509(signatures[0]) ?: return null
        return getCertificateSHA256Fingerprint(cert)
    }

    /**
     * Compares the app's first signature fingerprint with Bazaar's first signature fingerprint.
     * Useful when linking trust between app and Bazaar (optional).
     */
    fun compareAppWithBazaarSignature(context: Context): Boolean {
        val bazaarSignatures = getSignaturesSafe(context, BAZAAR_PACKAGE_NAME)
        val appSignatures = getSignaturesSafe(context, context.packageName)
        if (bazaarSignatures.isEmpty() || appSignatures.isEmpty()) return false

        val bazaarCert = signatureToX509(bazaarSignatures[0]) ?: return false
        val appCert = signatureToX509(appSignatures[0]) ?: return false

        return getCertificateSHA256Fingerprint(bazaarCert) == getCertificateSHA256Fingerprint(appCert)
    }

    /**
     * Generates full integrity report: Bazaar verification + app fingerprint + device state.
     */
    fun generateFullIntegrityReport(context: Context): String {
        val bazaarVerified = verifyBazaarIsInstalled(context)
        val rooted = isDeviceRooted()
        val debuggable = isAppDebuggable(context)
        val appFingerprint = getOwnAppSignatureFingerprint(context) ?: "N/A"

        return buildString {
            appendLine("🧭 Full Integrity Report")
            appendLine("===================================")
            appendLine("📦 Bazaar Verified: $bazaarVerified")
            appendLine("🔐 App Debuggable: $debuggable")
            appendLine("⚠️  Device Rooted: $rooted")
            appendLine("🔏 App Fingerprint (SHA-256): $appFingerprint")
            appendLine("🕒 Checked at: ${Date()}")
            appendLine("===================================")
        }
    }

    // =======================================================
    // 🎨 Lightweight UI Helpers (visually improved display)
    // =======================================================

    /**
     * Shows a short Toast with a security summary (not blocking).
     */
    fun showSecurityToast(context: Context) {
        val bazaarOk = verifyBazaarIsInstalled(context)
        val msg = if (bazaarOk) "Bazaar verified ✅" else "Bazaar verification failed ❌"
        Toast.makeText(context.applicationContext, msg, Toast.LENGTH_SHORT).show()
    }

    /**
     * Binds a generated full integrity report into a TextView with a pleasing style.
     * The TextView will be styled as a rounded card with gradient depending on verification state.
     */
    fun bindReportToTextView(context: Context, textView: TextView) {
        val report = generateFullIntegrityReport(context)
        textView.text = report
        textView.typeface = Typeface.MONOSPACE
        textView.textSize = 12f
        textView.setTextColor(Color.WHITE)
        textView.setPadding(24, 24, 24, 24)

        val bazaarOk = verifyBazaarIsInstalled(context)
        val startColor = if (bazaarOk) Color.parseColor("#4CAF50") else Color.parseColor("#F44336")
        val endColor = if (bazaarOk) Color.parseColor("#388E3C") else Color.parseColor("#D32F2F")

        val drawable = GradientDrawable(
            GradientDrawable.Orientation.LEFT_RIGHT,
            intArrayOf(startColor, endColor)
        ).apply {
            cornerRadius = 16f * context.resources.displayMetrics.density
        }

        textView.background = drawable
        textView.elevation = 8f
        textView.visibility = View.VISIBLE
    }

    /**
     * Copies the current full integrity report to clipboard and returns whether succeeded.
     */
    fun copyReportToClipboard(context: Context): Boolean {
        return try {
            val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
            val report = generateFullIntegrityReport(context)
            val clip = ClipData.newPlainText("Poolakey Security Report", report)
            cm.setPrimaryClip(clip)
            true
        } catch (e: Exception) {
            Log.w(TAG, "copyReportToClipboard failed: ${e.message}")
            false
        }
    }
}
package ir.cafebazaar.poolakey.request

import android.os.Bundle
import android.util.Base64
import ir.cafebazaar.poolakey.constant.BazaarIntent
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.*
import org.json.JSONObject

data class PurchaseRequest(
    val productId: String,
    val payload: String? = null,
    val dynamicPriceToken: String? = null
) {

    internal var cutoutModeIsShortEdges = false

    // ===============================
    // ✅ VALIDATION & UTILITY
    // ===============================

    /** Checks that the request has a valid product ID. */
    fun isValid(): Boolean = productId.isNotBlank()

    /** Generates a unique payload if none exists. */
    fun generatePayloadIfEmpty(): PurchaseRequest =
        if (payload.isNullOrEmpty()) copy(payload = UUID.randomUUID().toString()) else this

    /** Returns the payload encoded as Base64 safely. */
    fun payloadBase64(): String? =
        payload?.toByteArray(StandardCharsets.UTF_8)?.let { Base64.encodeToString(it, Base64.NO_WRAP) }

    /** Enable or disable cutout mode (short edges) for UI. */
    fun enableCutoutModeShortEdges(enable: Boolean = true): PurchaseRequest {
        cutoutModeIsShortEdges = enable
        return this
    }

    /** Converts the request into a Bundle suitable for Bazaar SDK. */
    internal fun purchaseExtraData(): Bundle =
        Bundle().apply {
            putString(BazaarIntent.RESPONSE_DYNAMIC_PRICE_TOKEN, dynamicPriceToken)
            putBoolean(BazaarIntent.RESPONSE_CUTOUT_MODE_IS_SHORT_EDGES, cutoutModeIsShortEdges)
            putString(BazaarIntent.RESPONSE_PRODUCT_ID, productId)
            payload?.let { putString(BazaarIntent.RESPONSE_PAYLOAD, it) }
        }

    /** Human-readable debug summary with improved formatting. */
    fun debugSummary(): String =
        buildString {
            appendLine("🛒 PurchaseRequest Summary:")
            appendLine("───────────────────────────────")
            appendLine("Product ID          : $productId")
            appendLine("Payload             : ${payload ?: "N/A"}")
            appendLine("Dynamic Price Token : ${dynamicPriceToken ?: "N/A"}")
            appendLine("Cutout Mode Short Edges : $cutoutModeIsShortEdges")
            appendLine("───────────────────────────────")
        }

    // ===============================
    // 🔐 SECURITY & INTEGRITY
    // ===============================

    /** SHA-256 hash of the payload (empty string if null). */
    fun payloadSHA256(): String {
        val data = payload ?: ""
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest(data.toByteArray(StandardCharsets.UTF_8))
        return hash.joinToString("") { "%02x".format(it) }
    }

    /**
     * Checks whether the dynamic price token is still valid.
     * Default expiry: 15 minutes (900,000 ms)
     */
    fun isDynamicPriceTokenValid(expiryMillis: Long = 15 * 60 * 1000): Boolean {
        return try {
            val decoded = dynamicPriceToken?.let { String(Base64.decode(it, Base64.DEFAULT)) } ?: return false
            val timestamp = decoded.toLongOrNull() ?: return false
            System.currentTimeMillis() - timestamp < expiryMillis
        } catch (_: Exception) {
            false
        }
    }

    // ===============================
    // 🌐 SERIALIZATION
    // ===============================

    /** Converts the request to JSON for logging/network use. */
    fun toJson(): JSONObject = JSONObject().apply {
        put("productId", productId)
        put("payload", payload ?: JSONObject.NULL)
        put("dynamicPriceToken", dynamicPriceToken ?: JSONObject.NULL)
        put("cutoutModeIsShortEdges", cutoutModeIsShortEdges)
    }

    companion object {
        /** Builder function that creates a request with a fresh payload automatically. */
        fun create(productId: String, dynamicPriceToken: String? = null): PurchaseRequest =
            PurchaseRequest(productId, payload = UUID.randomUUID().toString(), dynamicPriceToken = dynamicPriceToken)
    }
}

/** Creates a copy with a new dynamic price token. */
internal fun PurchaseRequest.withDynamicPriceToken(token: String): PurchaseRequest =
    copy(dynamicPriceToken = token)

/** Human-readable one-line summary for logs or UI. */
internal fun PurchaseRequest.summaryLine(): String =
    "🛍 Product: $productId | Payload: ${payload ?: "N/A"} | Token: ${dynamicPriceToken ?: "N/A"} | Cutout: $cutoutModeIsShortEdges"
package ir.cafebazaar.poolakey.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

internal class BillingReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent == null) return

        val broadcastIntent = Intent().apply {
            action = intent.action?.let { "$it.iab" }
            intent.extras?.let { bundle -> putExtras(bundle) }
        }

        notifyObserversSafely(broadcastIntent)
    }

    /**
     * Notify observers safely with filtering, priority, and timeout cleanup.
     */
    private fun notifyObserversSafely(intent: Intent) {
        val now = System.currentTimeMillis()

        synchronized(observerLock) {
            val iterator = observerEntries.iterator()

            while (iterator.hasNext()) {
                val entry = iterator.next()

                // Remove observer if timed out
                if (now - (observerTimestamps[entry.communicator] ?: 0) > OBSERVER_TIMEOUT_MS) {
                    Log.w(TAG, "Observer timed out and removed: ${entry.communicator}")
                    iterator.remove()
                    observerTimestamps.remove(entry.communicator)
                    continue
                }

                // Notify if action matches or filter is null (all)
                if (entry.filterAction == null || entry.filterAction == intent.action) {
                    try {
                        entry.communicator.onNewBroadcastReceived(intent)
                        observerTimestamps[entry.communicator] = now
                    } catch (e: Exception) {
                        Log.w(TAG, "Observer exception removed: ${entry.communicator} | ${e.message}")
                        iterator.remove()
                        observerTimestamps.remove(entry.communicator)
                    }
                }
            }
        }
    }

    companion object {
        private const val TAG = "BillingReceiver"
        private val observerLock = Any()
        private val observerEntries = mutableListOf<ObserverEntry>()
        private val observerTimestamps = mutableMapOf<BillingReceiverCommunicator, Long>()
        private const val OBSERVER_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes

        // =========================================
        // Observer Management
        // =========================================

        fun addObserver(
            communicator: BillingReceiverCommunicator,
            filterAction: String? = null,
            priority: Int = 0
        ) {
            synchronized(observerLock) {
                if (observerEntries.none { it.communicator == communicator }) {
                    observerEntries.add(ObserverEntry(communicator, filterAction, priority))
                    observerEntries.sortByDescending { it.priority } // Higher priority first
                    observerTimestamps[communicator] = System.currentTimeMillis()
                    Log.i(TAG, "Observer added: $communicator | Filter: ${filterAction ?: "ALL"} | Priority: $priority")
                }
            }
        }

        fun removeObserver(communicator: BillingReceiverCommunicator) {
            synchronized(observerLock) {
                observerEntries.removeAll { it.communicator == communicator }
                observerTimestamps.remove(communicator)
                Log.i(TAG, "Observer removed: $communicator")
            }
        }

        fun clearObservers() {
            synchronized(observerLock) {
                observerEntries.clear()
                observerTimestamps.clear()
                Log.i(TAG, "All observers cleared")
            }
        }

        fun listObservers(): List<BillingReceiverCommunicator> {
            synchronized(observerLock) {
                return observerEntries.map { it.communicator }.toList()
            }
        }

        fun isObserverRegistered(communicator: BillingReceiverCommunicator): Boolean {
            synchronized(observerLock) {
                return observerEntries.any { it.communicator == communicator }
            }
        }

        /**
         * Print a visually appealing observer table in Logcat
         */
        fun printRegisteredObservers() {
            synchronized(observerLock) {
                Log.i(TAG, "================ Registered Observers ================")
                if (observerEntries.isEmpty()) {
                    Log.i(TAG, "No observers registered")
                } else {
                    observerEntries.forEachIndexed { index, entry ->
                        val lastSeen = observerTimestamps[entry.communicator] ?: 0
                        val timeAgoSec = ((System.currentTimeMillis() - lastSeen) / 1000)
                        Log.i(
                            TAG, String.format(
                                "%2d | %-25s | Filter: %-10s | Priority: %2d | LastSeen: %4ds ago",
                                index + 1,
                                entry.communicator.toString(),
                                entry.filterAction ?: "ALL",
                                entry.priority,
                                timeAgoSec
                            )
                        )
                    }
                }
                Log.i(TAG, "=====================================================")
            }
        }
    }

    private data class ObserverEntry(
        val communicator: BillingReceiverCommunicator,
        val filterAction: String? = null,
        val priority: Int = 0
    )
}
package ir.cafebazaar.poolakey.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

internal class BillingReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        intent ?: return

        // Append ".iab" to the action and copy extras
        val broadcastIntent = Intent().apply {
            action = intent.action?.let { "$it.iab" }
            intent.extras?.let { putExtras(it) }
        }

        notifyObserversSafely(broadcastIntent)
    }

    /**
     * Notify observers safely:
     * - Remove observers that throw exceptions
     * - Remove observers that have timed out
     * - Respect filterAction and priority
     */
    private fun notifyObserversSafely(intent: Intent) {
        val now = System.currentTimeMillis()

        synchronized(observerLock) {
            val iterator = observerEntries.iterator()
            while (iterator.hasNext()) {
                val entry = iterator.next()

                // Remove timed-out observers
                if (now - (observerTimestamps[entry.communicator] ?: 0) > OBSERVER_TIMEOUT_MS) {
                    Log.w(TAG, "Observer timed out and removed: ${entry.communicator}")
                    iterator.remove()
                    observerTimestamps.remove(entry.communicator)
                    continue
                }

                // Notify if action matches filter or no filter
                if (entry.filterAction == null || entry.filterAction == intent.action) {
                    try {
                        entry.communicator.onNewBroadcastReceived(intent)
                        observerTimestamps[entry.communicator] = now
                    } catch (e: Exception) {
                        Log.w(TAG, "Observer exception removed: ${entry.communicator} | ${e.message}")
                        iterator.remove()
                        observerTimestamps.remove(entry.communicator)
                    }
                }
            }
        }
    }

    companion object {
        private const val TAG = "BillingReceiver"
        private val observerLock = Any()
        private val observerEntries = mutableListOf<ObserverEntry>()
        private val observerTimestamps = mutableMapOf<BillingReceiverCommunicator, Long>()
        private const val OBSERVER_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes

        // ===========================
        // Observer Management Methods
        // ===========================

        fun addObserver(
            communicator: BillingReceiverCommunicator,
            filterAction: String? = null,
            priority: Int = 0
        ) {
            synchronized(observerLock) {
                if (observerEntries.none { it.communicator == communicator }) {
                    observerEntries.add(ObserverEntry(communicator, filterAction, priority))
                    observerEntries.sortByDescending { it.priority } // Highest priority first
                    observerTimestamps[communicator] = System.currentTimeMillis()
                    Log.i(TAG, "Observer added: $communicator | Filter: ${filterAction ?: "ALL"} | Priority: $priority")
                }
            }
        }

        fun removeObserver(communicator: BillingReceiverCommunicator) {
            synchronized(observerLock) {
                observerEntries.removeAll { it.communicator == communicator }
                observerTimestamps.remove(communicator)
                Log.i(TAG, "Observer removed: $communicator")
            }
        }

        fun clearObservers() {
            synchronized(observerLock) {
                observerEntries.clear()
                observerTimestamps.clear()
                Log.i(TAG, "All observers cleared")
            }
        }

        fun listObservers(): List<BillingReceiverCommunicator> {
            synchronized(observerLock) {
                return observerEntries.map { it.communicator }
            }
        }

        fun isObserverRegistered(communicator: BillingReceiverCommunicator): Boolean {
            synchronized(observerLock) {
                return observerEntries.any { it.communicator == communicator }
            }
        }

        /**
         * Print a visually appealing observer table in Logcat
         */
        fun printRegisteredObservers() {
            synchronized(observerLock) {
                Log.i(TAG, "================ Registered Observers ================")
                if (observerEntries.isEmpty()) {
                    Log.i(TAG, "No observers registered")
                } else {
                    observerEntries.forEachIndexed { index, entry ->
                        val lastSeen = observerTimestamps[entry.communicator] ?: 0
                        val timeAgoSec = ((System.currentTimeMillis() - lastSeen) / 1000)
                        Log.i(
                            TAG, String.format(
                                "%2d | %-35s | Filter: %-10s | Priority: %2d | LastSeen: %4ds ago",
                                index + 1,
                                entry.communicator.toString(),
                                entry.filterAction ?: "ALL",
                                entry.priority,
                                timeAgoSec
                            )
                        )
                    }
                }
                Log.i(TAG, "=====================================================")
            }
        }
    }

    private data class ObserverEntry(
        val communicator: BillingReceiverCommunicator,
        val filterAction: String? = null,
        val priority: Int = 0
    )
}

// ========================================================
// BillingReceiverCommunicator Interface
// ========================================================

internal interface BillingReceiverCommunicator {
    fun onNewBroadcastReceived(intent: Intent?)
}
package ir.cafebazaar.poolakey.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

internal class BillingReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        intent ?: return

        // Append ".iab" to the action and copy extras
        val broadcastIntent = Intent().apply {
            action = intent.action?.let { "$it.iab" }
            intent.extras?.let { putExtras(it) }
        }

        notifyObserversSafely(broadcastIntent)
    }

    /**
     * Notify observers safely:
     * - Remove observers that throw exceptions
     * - Remove observers that have timed out
     * - Respect filterAction and priority
     */
    private fun notifyObserversSafely(intent: Intent) {
        val now = System.currentTimeMillis()

        synchronized(observerLock) {
            val iterator = observerEntries.iterator()
            while (iterator.hasNext()) {
                val entry = iterator.next()

                // Remove timed-out observers
                if (now - (observerTimestamps[entry.communicator] ?: 0) > OBSERVER_TIMEOUT_MS) {
                    Log.w(TAG, "Observer timed out and removed: ${entry.communicator}")
                    iterator.remove()
                    observerTimestamps.remove(entry.communicator)
                    continue
                }

                // Notify if action matches filter or no filter
                if (entry.filterAction == null || entry.filterAction == intent.action) {
                    try {
                        entry.communicator.onNewBroadcastReceived(intent)
                        observerTimestamps[entry.communicator] = now
                    } catch (e: Exception) {
                        Log.w(TAG, "Observer exception removed: ${entry.communicator} | ${e.message}")
                        iterator.remove()
                        observerTimestamps.remove(entry.communicator)
                    }
                }
            }
        }
    }

    companion object {
        private const val TAG = "BillingReceiver"
        private val observerLock = Any()
        private val observerEntries = mutableListOf<ObserverEntry>()
        private val observerTimestamps = mutableMapOf<BillingReceiverCommunicator, Long>()
        private const val OBSERVER_TIMEOUT_MS = 10 * 60 * 1000L // 10 minutes

        // ===========================
        // Observer Management Methods
        // ===========================

        fun addObserver(
            communicator: BillingReceiverCommunicator,
            filterAction: String? = null,
            priority: Int = 0
        ) {
            synchronized(observerLock) {
                if (observerEntries.none { it.communicator == communicator }) {
                    observerEntries.add(ObserverEntry(communicator, filterAction, priority))
                    observerEntries.sortByDescending { it.priority } // Highest priority first
                    observerTimestamps[communicator] = System.currentTimeMillis()
                    Log.i(TAG, "Observer added: $communicator | Filter: ${filterAction ?: "ALL"} | Priority: $priority")
                }
            }
        }

        fun removeObserver(communicator: BillingReceiverCommunicator) {
            synchronized(observerLock) {
                observerEntries.removeAll { it.communicator == communicator }
                observerTimestamps.remove(communicator)
                Log.i(TAG, "Observer removed: $communicator")
            }
        }

        fun clearObservers() {
            synchronized(observerLock) {
                observerEntries.clear()
                observerTimestamps.clear()
                Log.i(TAG, "All observers cleared")
            }
        }

        fun listObservers(): List<BillingReceiverCommunicator> {
            synchronized(observerLock) {
                return observerEntries.map { it.communicator }
            }
        }

        fun isObserverRegistered(communicator: BillingReceiverCommunicator): Boolean {
            synchronized(observerLock) {
                return observerEntries.any { it.communicator == communicator }
            }
        }

        /**
         * Print a visually appealing observer table in Logcat
         */
        fun printRegisteredObservers() {
            synchronized(observerLock) {
                Log.i(TAG, "================ Registered Observers ================")
                if (observerEntries.isEmpty()) {
                    Log.i(TAG, "No observers registered")
                } else {
                    observerEntries.forEachIndexed { index, entry ->
                        val lastSeen = observerTimestamps[entry.communicator] ?: 0
                        val timeAgoSec = ((System.currentTimeMillis() - lastSeen) / 1000)
                        Log.i(
                            TAG, String.format(
                                "%2d | %-35s | Filter: %-10s | Priority: %2d | LastSeen: %4ds ago",
                                index + 1,
                                entry.communicator.toString(),
                                entry.filterAction ?: "ALL",
                                entry.priority,
                                timeAgoSec
                            )
                        )
                    }
                }
                Log.i(TAG, "=====================================================")
            }
        }
    }

    private data class ObserverEntry(
        val communicator: BillingReceiverCommunicator,
        val filterAction: String? = null,
        val priority: Int = 0
    )
}

// ========================================================
// BillingReceiverCommunicator Interface
// ========================================================

internal interface BillingReceiverCommunicator {
    fun onNewBroadcastReceived(intent: Intent?)
}
package ir.cafebazaar.poolakey.mapper

import ir.cafebazaar.poolakey.constant.RawJson
import ir.cafebazaar.poolakey.entity.PurchaseInfo
import ir.cafebazaar.poolakey.entity.PurchaseState
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*

internal class RawDataToPurchaseInfo {

    fun mapToPurchaseInfo(purchaseData: String, dataSignature: String): PurchaseInfo {
        return JSONObject(purchaseData).run {
            PurchaseInfo(
                orderId = optString(RawJson.ORDER_ID),
                purchaseToken = optString(RawJson.PURCHASE_TOKEN),
                payload = optString(RawJson.DEVELOPER_PAYLOAD),
                packageName = optString(RawJson.PACKAGE_NAME),
                purchaseState = if (optInt(RawJson.PURCHASE_STATE) == 0) PurchaseState.PURCHASED else PurchaseState.REFUNDED,
                purchaseTime = optLong(RawJson.PURCHASE_TIME),
                productId = optString(RawJson.PRODUCT_ID),
                dataSignature = dataSignature,
                originalJson = purchaseData
            )
        }
    }

    fun mapList(purchases: List<Pair<String, String>>): List<PurchaseInfo> {
        return purchases.map { (data, signature) -> mapToPurchaseInfo(data, signature) }
    }

    fun getPurchaseSummary(purchaseData: String): String {
        val json = JSONObject(purchaseData)
        val state = if (json.optInt(RawJson.PURCHASE_STATE) == 0) "✅ PURCHASED" else "❌ REFUNDED"
        val formattedTime = formatPurchaseTime(purchaseData)
        return buildString {
            appendLine("━━━━━━━━━ Purchase Summary ━━━━━━━━━")
            appendLine("Order ID     : ${json.optString(RawJson.ORDER_ID)}")
            appendLine("Product ID   : ${json.optString(RawJson.PRODUCT_ID)}")
            appendLine("Purchase State: $state")
            appendLine("Purchase Time: $formattedTime")
            appendLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
        }
    }

    fun isRefunded(purchaseData: String): Boolean {
        return JSONObject(purchaseData).optInt(RawJson.PURCHASE_STATE) != 0
    }

    fun getField(purchaseData: String, field: String): String? {
        val json = JSONObject(purchaseData)
        return json.optString(field, null)
    }

    /**
     * Placeholder for verifying purchase signature.
     */
    fun verifyPurchaseSignature(purchaseData: String, signature: String, publicKey: String): Boolean {
        if (signature.isBlank() || purchaseData.isBlank()) {
            println("⚠️ Signature verification placeholder - not implemented!")
            return false
        }
        return true
    }

    fun filterByState(purchases: List<PurchaseInfo>, state: PurchaseState): List<PurchaseInfo> {
        return purchases.filter { it.purchaseState == state }
    }

    fun getMostRecentPurchase(purchases: List<PurchaseInfo>): PurchaseInfo? {
        return purchases.maxByOrNull { it.purchaseTime }
    }

    fun formatPurchaseTime(purchaseData: String): String {
        val time = JSONObject(purchaseData).optLong(RawJson.PURCHASE_TIME)
        return if (time > 0) {
            SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(time))
        } else {
            "N/A"
        }
    }

    fun isPurchaseForProduct(purchaseData: String, productId: String): Boolean {
        return JSONObject(purchaseData).optString(RawJson.PRODUCT_ID) == productId
    }
}
package ir.cafebazaar.poolakey.exception

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.text.style.TypefaceSpan
import android.util.Log
import android.widget.Toast
import org.json.JSONObject
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*

/**
 * Thrown when an operation is intentionally aborted or interrupted.
 * Extends [InterruptedException] for compatibility with thread interruption logic.
 *
 * @Property operation The name of the operation that was aborted (optional)
 * @Property reason The explanation or cause of abortion (optional)
 * @Property timestamp The system time when the abortion occurred.
 */
class AbortedException(
    val operation: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis(),
    message: String? = null,
    causeThrowable: Throwable? = null
) : InterruptedException(message ?: buildMessage(operation, reason)) {

    init {
        // Attach underlying cause to the Throwable if provided
        causeThrowable?.let { initCause(it) }
    }

    companion object {
        private const val TAG = "AbortedException"

        private fun buildMessage(operation: String?, reason: String?): String {
            return when {
                operation != null && reason != null ->
                    "Operation '$operation' was aborted: $reason"
                operation != null ->
                    "Operation '$operation' was aborted."
                reason != null ->
                    "Operation aborted: $reason"
                else ->
                    "Operation aborted unexpectedly."
            }
        }

        /**
         * Creates an [AbortedException] for user cancellation events.
         */
        fun forUserCancel(operation: String? = null): AbortedException {
            return AbortedException(
                operation = operation,
                reason = "User cancelled the operation.",
                message = "User cancelled ${operation ?: "an operation"}"
            )
        }

        /**
         * Creates an [AbortedException] for timeout scenarios.
         */
        fun forTimeout(operation: String? = null, durationMs: Long? = null): AbortedException {
            val readable = durationMs?.let { "${it / 1000}s" } ?: "unknown"
            return AbortedException(
                operation = operation,
                reason = "Operation timed out after $readable.",
                message = "Timeout occurred during ${operation ?: "an operation"}"
            )
        }

        /**
         * Creates an [AbortedException] due to network issues.
         */
        fun forNetworkError(operation: String? = null): AbortedException {
            return AbortedException(
                operation = operation,
                reason = "Network connection lost or unavailable.",
                message = "Network error during ${operation ?: "operation"}"
            )
        }
    }

    /**
     * Returns a detailed, user-friendly description of the abortion event.
     */
    fun describe(): String {
        return buildString {
            appendLine("⚠️ AbortedException Details ⚠️")
            appendLine("Timestamp : ${formatTimestamp(timestamp)}")
            appendLine("Operation : ${operation ?: "Unknown"}")
            appendLine("Reason    : ${reason ?: "Unspecified"}")
            appendLine("Message   : ${message ?: "No message provided"}")
            [email protected]?.let { appendLine("Caused by : ${it.javaClass.simpleName}: ${it.message}") }
        }
    }

    /**
     * Logs this exception in a clean, structured, and visually enhanced way.
     */
    fun log(tag: String = TAG) {
        Log.e(tag, "❌ ${message ?: "AbortedException occurred"}")
        Log.e(tag, "• Operation: ${operation ?: "Unknown"}")
        Log.e(tag, "• Reason   : ${reason ?: "Unspecified"}")
        Log.e(tag, "• Time     : ${formatTimestamp(timestamp)}")
        this.cause?.let {
            Log.e(tag, "• Cause    : ${it.javaClass.simpleName}: ${it.message}")
            Log.d(tag, getStackTraceAsString(it))
        }
    }

    /**
     * Returns true if the abortion reason is due to user cancellation.
     */
    fun isUserCancelled(): Boolean {
        return reason?.contains("cancel", ignoreCase = true) == true
    }

    /**
     * Suggests possible recovery actions based on the abortion context.
     */
    fun getRecoverySuggestion(): String {
        return when {
            isUserCancelled() -> "User canceled the operation. No further action needed."
            reason?.contains("timeout", ignoreCase = true) == true -> "Try increasing the timeout duration and retry."
            reason?.contains("network", ignoreCase = true) == true -> "Please check your internet connection and retry."
            else -> "Check logs or contact support if this issue persists."
        }
    }

    /**
     * Converts the stack trace of the cause (if any) into a string for logging.
     */
    private fun getStackTraceAsString(throwable: Throwable): String {
        val writer = StringWriter()
        throwable.printStackTrace(PrintWriter(writer))
        return writer.toString()
    }

    /**
     * Converts the timestamp into a human-readable date/time format.
     */
    private fun formatTimestamp(time: Long): String {
        val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
        return sdf.format(Date(time))
    }

    /**
     * Converts this exception into a map structure for analytics or crash reporting.
     */
    fun toMap(): Map<String, Any?> {
        return mapOf(
            "operation" to operation,
            "reason" to reason,
            "timestamp" to timestamp,
            "message" to message,
            "isUserCancelled" to isUserCancelled(),
            "recoverySuggestion" to getRecoverySuggestion(),
            "cause" to this.cause?.javaClass?.simpleName
        )
    }

    /**
     * Converts this exception to JSON for structured logging or remote reporting.
     * Uses JSONObject to ensure proper escaping of strings.
     */
    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject()
        obj.put("operation", operation ?: JSONObject.NULL)
        obj.put("reason", reason ?: JSONObject.NULL)
        obj.put("timestamp", timestamp)
        obj.put("message", message ?: JSONObject.NULL)
        obj.put("isUserCancelled", isUserCancelled())
        obj.put("recoverySuggestion", getRecoverySuggestion())
        obj.put("cause", this.cause?.javaClass?.simpleName ?: JSONObject.NULL)
        return if (pretty) obj.toString(2) else obj.toString()
    }

    override fun toString(): String {
        return "AbortedException(operation=$operation, reason=$reason, timestamp=$timestamp, message=$message)"
    }

    // =======================================================
    // 🎨 UI Helpers — these make presenting the exception more pleasant
    // (lightweight helpers that don't force UI dependencies)
    // =======================================================

    /**
     * Returns a styled [SpannableString] suitable for showing in a TextView.
     * Title (operation) will be bold and primary colored; body will be secondary colored; time will be dim + italic.
     *
     * Example usage:
     * textView.text = abortedException.toSpannableMessage(primaryColor, secondaryColor)
     */
    fun toSpannableMessage(primaryColor: Int, secondaryColor: Int): SpannableString {
        val title = operation ?: "Operation aborted"
        val body = reason ?: (message ?: "The operation was aborted.")
        val time = formatTimestamp(timestamp)
        val full = "$title\n$body\n$time"
        val spannable = SpannableString(full)

        // title bold + primary color
        spannable.setSpan(StyleSpan(android.graphics.Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        spannable.setSpan(ForegroundColorSpan(primaryColor), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

        // body colored secondary
        val bodyStart = title.length + 1
        val bodyEnd = bodyStart + body.length
        if (bodyStart < bodyEnd) {
            spannable.setSpan(ForegroundColorSpan(secondaryColor), bodyStart, bodyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }

        // time dim + italic
        val timeStart = bodyEnd + 1
        val timeEnd = full.length
        if (timeStart < timeEnd) {
            spannable.setSpan(ForegroundColorSpan(adjustAlpha(secondaryColor, 0.7f)), timeStart, timeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
            spannable.setSpan(StyleSpan(android.graphics.Typeface.ITALIC), timeStart, timeEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }

        return spannable
    }

    private fun adjustAlpha(@androidx.annotation.ColorInt color: Int, factor: Float): Int {
        val a = (android.graphics.Color.alpha(color) * factor).toInt()
        val r = android.graphics.Color.red(color)
        val g = android.graphics.Color.green(color)
        val b = android.graphics.Color.blue(color)
        return android.graphics.Color.argb(a, r, g, b)
    }

    /**
     * Shows a short Toast to the user with a friendly message extracted from this exception.
     * Safe to call from any thread (it will post to main Looper).
     */
    fun showAsToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
        val toastText = when {
            isUserCancelled() -> "Action cancelled."
            reason != null -> reason
            message != null -> message
            else -> "Operation aborted."
        } ?: "Operation aborted."

        if (Looper.myLooper() == Looper.getMainLooper()) {
            Toast.makeText(context.applicationContext, toastText, duration).show()
        } else {
            Handler(Looper.getMainLooper()).post {
                Toast.makeText(context.applicationContext, toastText, duration).show()
            }
        }
    }
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*

/**
 * Thrown when the Bazaar app (Cafebazaar) is not found on the user's device.
 * This typically occurs when trying to use in-app billing APIs or intents that require Bazaar.
 */
class BazaarNotFoundException(
    val packageName: String = "com.farsitel.bazaar",
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please install or update Bazaar to continue."
) : IllegalStateException() {

    override val message: String?
        get() = "Bazaar is not installed"

    companion object {
        private const val TAG = "BazaarNotFoundException"

        /**
         * Creates a default instance when Bazaar is missing.
         */
        fun create(): BazaarNotFoundException = BazaarNotFoundException()

        /**
         * Returns the official Bazaar install URI.
         */
        fun getInstallUri(): Uri = Uri.parse("bazaar://details?id=com.farsitel.bazaar")

        /**
         * Returns a web fallback URL for Bazaar download.
         */
        fun getWebInstallUri(): Uri =
            Uri.parse("https://cafebazaar.ir/app/com.farsitel.bazaar?l=en")

        /**
         * Checks whether Bazaar is installed on the device.
         */
        fun isBazaarInstalled(context: Context): Boolean {
            return try {
                context.packageManager.getPackageInfo("com.farsitel.bazaar", 0)
                true
            } catch (e: Exception) {
                false
            }
        }
    }

    // ===============================================================
    // 📋 Helper Methods
    // ===============================================================

    fun describe(): String {
        val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
        return buildString {
            appendLine("⚠️ BazaarNotFoundException")
            appendLine("Time      : $date")
            appendLine("Message   : ${message ?: "Unknown error"}")
            appendLine("Package   : $packageName")
            appendLine("Hint      : ${recoveryHint ?: "Install Bazaar manually"}")
        }
    }

    fun log(tag: String = TAG) {
        Log.e(tag, "❌ BazaarNotFoundException: ${message}")
        Log.e(tag, "• Package: $packageName")
        Log.e(tag, "• Time   : ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))}")
        Log.e(tag, "• Hint   : ${recoveryHint ?: "Install Bazaar manually"}")
    }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject()
        obj.put("error", "BazaarNotFoundException")
        obj.put("message", message)
        obj.put("timestamp", timestamp)
        obj.put("package", packageName)
        obj.put("recoveryHint", recoveryHint)
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun getRecoverySuggestion(): String =
        recoveryHint ?: "Please install or update Bazaar and try again."

    // ===============================================================
    // 🎨 User Interface Helpers
    // ===============================================================

    fun toSpannableMessage(primaryColor: Int, secondaryColor: Int): SpannableString {
        val title = "⚠️ Bazaar Not Installed"
        val body = recoveryHint ?: "Please install Bazaar to continue."
        val fullText = "$title\n\n$body"
        val spannable = SpannableString(fullText)

        spannable.setSpan(StyleSpan(Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        spannable.setSpan(ForegroundColorSpan(primaryColor), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        val bodyStart = title.length + 2
        if (bodyStart < fullText.length) {
            spannable.setSpan(ForegroundColorSpan(secondaryColor), bodyStart, fullText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }
        return spannable
    }

    fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
        val messageToShow = "⚠️ " + getRecoverySuggestion()
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Toast.makeText(context.applicationContext, messageToShow, duration).show()
        } else {
            Handler(Looper.getMainLooper()).post {
                Toast.makeText(context.applicationContext, messageToShow, duration).show()
            }
        }
    }

    /**
     * A beautiful and polished dialog prompting the user to install Bazaar.
     */
    fun showBeautifulDialog(
        context: Context,
        accentColor: Int = ContextCompat.getColor(context, android.R.color.holo_green_dark),
        onInstall: (() -> Unit)? = null,
        onCancel: (() -> Unit)? = null
    ) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            Handler(Looper.getMainLooper()).post {
                showBeautifulDialog(context, accentColor, onInstall, onCancel)
            }
            return
        }

        try {
            val dialogBuilder = AlertDialog.Builder(context)
            val spannableMsg = toSpannableMessage(accentColor, ContextCompat.getColor(context, android.R.color.darker_gray))
            dialogBuilder.setTitle("✨ Bazaar Required ✨")
                .setMessage(spannableMsg)
                .setIcon(android.R.drawable.ic_dialog_info)
                .setCancelable(false)
                .setPositiveButton("🚀 Install Bazaar") { dialog, _ ->
                    openBazaarInstallPage(context)
                    onInstall?.invoke()
                    dialog.dismiss()
                }
                .setNegativeButton("❌ Cancel") { dialog, _ ->
                    onCancel?.invoke()
                    dialog.dismiss()
                }
            dialogBuilder.create().show()
        } catch (e: Exception) {
            Log.w(TAG, "Dialog failed: ${e.message}")
            showAsToast(context)
        }
    }

    // ===============================================================
    // 🧠 Behavior & Recovery Functions
    // ===============================================================

    fun attemptRecovery(context: Context): Boolean {
        return if (!isBazaarInstalled(context)) {
            openBazaarInstallPage(context)
            true
        } else false
    }

    fun getLocalizedRecoveryHint(locale: Locale = Locale.getDefault()): String {
        return when (locale.language.lowercase(Locale.ROOT)) {
            "fa" -> "لطفاً برنامه بازار را نصب یا به‌روزرسانی کنید تا بتوانید ادامه دهید."
            else -> recoveryHint ?: "Please install or update Bazaar to continue."
        }
    }

    fun runIfBazaarAvailable(
        context: Context,
        onAvailable: () -> Unit,
        onMissing: (() -> Unit)? = null
    ) {
        if (isBazaarInstalled(context)) onAvailable()
        else onMissing?.invoke() ?: showBeautifulDialog(context)
    }

    fun openBazaarInstallPage(context: Context) {
        val pm: PackageManager = context.packageManager ?: return
        val intent = Intent(Intent.ACTION_VIEW, getInstallUri()).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }

        val resolved = pm.queryIntentActivities(intent, 0)
        if (resolved?.isNotEmpty() == true) {
            try {
                context.startActivity(intent)
                return
            } catch (e: Exception) {
                Log.w(TAG, "Failed to open Bazaar install page: ${e.message}")
            }
        }

        try {
            val webIntent = Intent(Intent.ACTION_VIEW, getWebInstallUri()).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
            context.startActivity(webIntent)
        } catch (e: Exception) {
            Log.e(TAG, "Could not launch Bazaar install fallback: ${e.message}")
        }
    }

    fun showInstallToastIfMissing(context: Context, duration: Int = Toast.LENGTH_LONG) {
        if (!isBazaarInstalled(context)) showAsToast(context, duration)
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "BazaarNotFoundException",
        "message" to message,
        "package" to packageName,
        "timestamp" to timestamp,
        "hint" to recoveryHint
    )

    fun toDebugString(): String {
        val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
        return "[BazaarMissing] ($time) ${message ?: "No message"}"
    }

    override fun toString(): String {
        return "BazaarNotFoundException(packageName=$packageName, message=$message, timestamp=$timestamp)"
    }
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*

/**
 * Thrown when the Bazaar app (Cafebazaar) is not found on the user's device.
 * This typically occurs when trying to use in-app billing APIs or intents that require Bazaar.
 */
class BazaarNotFoundException(
    val packageName: String = "com.farsitel.bazaar",
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please install or update Bazaar to continue."
) : IllegalStateException() {

    override val message: String?
        get() = "Bazaar is not installed"

    companion object {
        private const val TAG = "BazaarNotFoundException"

        /**
         * Creates a default instance when Bazaar is missing.
         */
        fun create(): BazaarNotFoundException = BazaarNotFoundException()

        /**
         * Returns the official Bazaar install URI.
         */
        fun getInstallUri(): Uri = Uri.parse("bazaar://details?id=com.farsitel.bazaar")

        /**
         * Returns a web fallback URL for Bazaar download.
         */
        fun getWebInstallUri(): Uri =
            Uri.parse("https://cafebazaar.ir/app/com.farsitel.bazaar?l=en")

        /**
         * Checks whether Bazaar is installed on the device.
         */
        fun isBazaarInstalled(context: Context): Boolean {
            return try {
                context.packageManager.getPackageInfo("com.farsitel.bazaar", 0)
                true
            } catch (e: Exception) {
                false
            }
        }
    }

    // ===============================================================
    // 📋 Helper Methods
    // ===============================================================

    fun describe(): String {
        val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
        return buildString {
            appendLine("⚠️ BazaarNotFoundException")
            appendLine("Time      : $date")
            appendLine("Message   : ${message ?: "Unknown error"}")
            appendLine("Package   : $packageName")
            appendLine("Hint      : ${recoveryHint ?: "Install Bazaar manually"}")
        }
    }

    fun log(tag: String = TAG) {
        Log.e(tag, "❌ BazaarNotFoundException: ${message}")
        Log.e(tag, "• Package: $packageName")
        Log.e(tag, "• Time   : ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))}")
        Log.e(tag, "• Hint   : ${recoveryHint ?: "Install Bazaar manually"}")
    }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject()
        obj.put("error", "BazaarNotFoundException")
        obj.put("message", message)
        obj.put("timestamp", timestamp)
        obj.put("package", packageName)
        obj.put("recoveryHint", recoveryHint)
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun getRecoverySuggestion(): String =
        recoveryHint ?: "Please install or update Bazaar and try again."

    // ===============================================================
    // 🎨 User Interface Helpers
    // ===============================================================

    fun toSpannableMessage(primaryColor: Int, secondaryColor: Int): SpannableString {
        val title = "⚠️ Bazaar Not Installed"
        val body = recoveryHint ?: "Please install Bazaar to continue."
        val fullText = "$title\n\n$body"
        val spannable = SpannableString(fullText)

        spannable.setSpan(StyleSpan(Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        spannable.setSpan(ForegroundColorSpan(primaryColor), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        val bodyStart = title.length + 2
        if (bodyStart < fullText.length) {
            spannable.setSpan(ForegroundColorSpan(secondaryColor), bodyStart, fullText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        }
        return spannable
    }

    fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
        val messageToShow = "⚠️ " + getRecoverySuggestion()
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Toast.makeText(context.applicationContext, messageToShow, duration).show()
        } else {
            Handler(Looper.getMainLooper()).post {
                Toast.makeText(context.applicationContext, messageToShow, duration).show()
            }
        }
    }

    /**
     * A beautiful and polished dialog prompting the user to install Bazaar.
     */
    fun showBeautifulDialog(
        context: Context,
        accentColor: Int = ContextCompat.getColor(context, android.R.color.holo_green_dark),
        onInstall: (() -> Unit)? = null,
        onCancel: (() -> Unit)? = null
    ) {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            Handler(Looper.getMainLooper()).post {
                showBeautifulDialog(context, accentColor, onInstall, onCancel)
            }
            return
        }

        try {
            val dialogBuilder = AlertDialog.Builder(context)
            val spannableMsg = toSpannableMessage(accentColor, ContextCompat.getColor(context, android.R.color.darker_gray))
            dialogBuilder.setTitle("✨ Bazaar Required ✨")
                .setMessage(spannableMsg)
                .setIcon(android.R.drawable.ic_dialog_info)
                .setCancelable(false)
                .setPositiveButton("🚀 Install Bazaar") { dialog, _ ->
                    openBazaarInstallPage(context)
                    onInstall?.invoke()
                    dialog.dismiss()
                }
                .setNegativeButton("❌ Cancel") { dialog, _ ->
                    onCancel?.invoke()
                    dialog.dismiss()
                }
            dialogBuilder.create().show()
        } catch (e: Exception) {
            Log.w(TAG, "Dialog failed: ${e.message}")
            showAsToast(context)
        }
    }

    // ===============================================================
    // 🧠 Behavior & Recovery Functions
    // ===============================================================

    fun attemptRecovery(context: Context): Boolean {
        return if (!isBazaarInstalled(context)) {
            openBazaarInstallPage(context)
            true
        } else false
    }

    fun getLocalizedRecoveryHint(locale: Locale = Locale.getDefault()): String {
        return when (locale.language.lowercase(Locale.ROOT)) {
            "fa" -> "لطفاً برنامه بازار را نصب یا به‌روزرسانی کنید تا بتوانید ادامه دهید."
            else -> recoveryHint ?: "Please install or update Bazaar to continue."
        }
    }

    fun runIfBazaarAvailable(
        context: Context,
        onAvailable: () -> Unit,
        onMissing: (() -> Unit)? = null
    ) {
        if (isBazaarInstalled(context)) onAvailable()
        else onMissing?.invoke() ?: showBeautifulDialog(context)
    }

    fun openBazaarInstallPage(context: Context) {
        val pm: PackageManager = context.packageManager ?: return
        val intent = Intent(Intent.ACTION_VIEW, getInstallUri()).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        }

        val resolved = pm.queryIntentActivities(intent, 0)
        if (resolved?.isNotEmpty() == true) {
            try {
                context.startActivity(intent)
                return
            } catch (e: Exception) {
                Log.w(TAG, "Failed to open Bazaar install page: ${e.message}")
            }
        }

        try {
            val webIntent = Intent(Intent.ACTION_VIEW, getWebInstallUri()).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
            context.startActivity(webIntent)
        } catch (e: Exception) {
            Log.e(TAG, "Could not launch Bazaar install fallback: ${e.message}")
        }
    }

    fun showInstallToastIfMissing(context: Context, duration: Int = Toast.LENGTH_LONG) {
        if (!isBazaarInstalled(context)) showAsToast(context, duration)
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "BazaarNotFoundException",
        "message" to message,
        "package" to packageName,
        "timestamp" to timestamp,
        "hint" to recoveryHint
    )

    fun toDebugString(): String {
        val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
        return "[BazaarMissing] ($time) ${message ?: "No message"}"
    }

    override fun toString(): String {
        return "BazaarNotFoundException(packageName=$packageName, message=$message, timestamp=$timestamp)"
    }
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*

/**
 * Exception thrown when Bazaar app is installed but not updated
 * to a version that supports the required billing or service features.
 */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String?
        get() = "Bazaar is not updated"

    companion object {
        private const val TAG = "BazaarNotSupportedException"
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"

        /** Bazaar in-app URI */
        fun getUpdateUri(): Uri = Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")

        /** Web fallback URI */
        fun getWebUpdateUri(): Uri =
            Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")

        /** Check if Bazaar is installed */
        fun isBazaarInstalled(context: Context): Boolean {
            return try {
                context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0)
                true
            } catch (_: PackageManager.NameNotFoundException) {
                false
            } catch (_: Exception) {
                false
            }
        }

        /** Get installed Bazaar version code (supports all API levels) */
        fun getInstalledVersionCode(context: Context): Long? {
            return try {
                val info = context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode
                else @Suppress("DEPRECATION") info.versionCode.toLong()
            } catch (_: Exception) {
                null
            }
        }

        /** Check if Bazaar supports the required version */
        fun isVersionSupported(context: Context, requiredVersion: Int): Boolean {
            val current = getInstalledVersionCode(context) ?: return false
            return current >= requiredVersion
        }
    }

    // ===============================================================
    // 📋 Core Diagnostic & Info Utilities
    // ===============================================================

    fun describe(): String {
        val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
        return """
            ⚠️ BazaarNotSupportedException
            ──────────────────────────────
            Time        : $date
            Message     : ${message ?: "Unknown"}
            Package     : $packageName
            Required Ver: ${requiredVersion ?: "Unknown"}
            Current Ver : ${currentVersion ?: "Unknown"}
            Hint        : ${recoveryHint ?: "Please update Bazaar manually"}
        """.trimIndent()
    }

    fun log(tag: String = TAG) {
        Log.e(tag, "❌ BazaarNotSupportedException: $message")
        Log.e(tag, "• Package: $packageName")
        Log.e(tag, "• Required version: ${requiredVersion ?: "?"}")
        Log.e(tag, "• Current version : ${currentVersion ?: "?"}")
        Log.e(tag, "• Hint : $recoveryHint")
    }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply {
            put("error", "BazaarNotSupportedException")
            put("message", message)
            put("timestamp", timestamp)
            put("package", packageName)
            put("requiredVersion", requiredVersion)
            put("currentVersion", currentVersion)
            put("recoveryHint", recoveryHint)
        }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun getRecoverySuggestion(): String =
        recoveryHint ?: "Please update Bazaar to the latest version."

    // ===============================================================
    // 🎨 Modern, Beautiful UI Helpers
    // ===============================================================

    fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
        val msg = getRecoverySuggestion()
        val runnable = Runnable {
            Toast.makeText(context.applicationContext, msg, duration).show()
        }
        if (Looper.myLooper() == Looper.getMainLooper()) runnable.run()
        else Handler(Looper.getMainLooper()).post(runnable)
    }

    /**
     * A stylish, user-friendly update dialog with better typography and color scheme.
     */
    fun showUpdateDialog(
        context: Context,
        onUpdate: (() -> Unit)? = null,
        onCancel: (() -> Unit)? = null
    ) {
        val showDialog = {
            try {
                val titleView = TextView(context).apply {
                    text = "⚠️ Bazaar Update Required"
                    textSize = 20f
                    setTypeface(null, Typeface.BOLD)
                    setTextColor(ContextCompat.getColor(context, android.R.color.holo_orange_dark))
                    setPadding(50, 40, 50, 20)
                }

                val messageView = TextView(context).apply {
                    text = """
                        Your version of Bazaar is outdated. 
                        Please update to continue using all features.

                        ${getRecoverySuggestion()}
                    """.trimIndent()
                    textSize = 16f
                    setTextColor(ContextCompat.getColor(context, android.R.color.secondary_text_dark))
                    setPadding(50, 0, 50, 20)
                }

                val layout = LinearLayout(context).apply {
                    orientation = LinearLayout.VERTICAL
                    addView(titleView)
                    addView(messageView)
                }

                val builder = AlertDialog.Builder(context)
                    .setView(layout)
                    .setCancelable(false)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context)
                        onUpdate?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(
                        ContextCompat.getColor(context, android.R.color.holo_green_light)
                    )
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(
                        ContextCompat.getColor(context, android.R.color.holo_red_light)
                    )
                }

                dialog.show()
            } catch (e: Exception) {
                Log.w(TAG, "Dialog failed: ${e.message}")
                showAsToast(context)
            }
        }

        if (Looper.myLooper() == Looper.getMainLooper()) showDialog()
        else Handler(Looper.getMainLooper()).post(showDialog)
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            val intent = Intent(Intent.ACTION_VIEW, getUpdateUri()).apply {
                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }
            context.startActivity(intent)
        } catch (_: ActivityNotFoundException) {
            // Fallback to web
            try {
                val webIntent = Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply {
                    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                }
                context.startActivity(webIntent)
            } catch (e: Exception) {
                Log.e(TAG, "Failed to open web update page: ${e.message}")
                showAsToast(context)
            }
        }
    }

    // ===============================================================
    // 🧩 Smart Logic & Helper Tools
    // ===============================================================

    fun hasInternetConnection(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network)
        return caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
    }

    fun needsUpdate(): Boolean =
        requiredVersion != null && currentVersion != null && currentVersion < requiredVersion

    fun elapsedSinceThrown(): Long =
        (System.currentTimeMillis() - timestamp) / 1000

    fun getReadableElapsedTime(): String {
        val seconds = elapsedSinceThrown()
        val minutes = seconds / 60
        val remaining = seconds % 60
        return if (minutes > 0) "${minutes}m ${remaining}s ago" else "${remaining}s ago"
    }

    fun getLocalizedRecoveryHint(locale: Locale = Locale.getDefault()): String =
        when (locale.language.lowercase(Locale.getDefault())) {
            "fa" -> "لطفاً بازار را به‌روزرسانی کنید تا بتوانید ادامه دهید."
            else -> recoveryHint ?: "Please update Bazaar to continue."
        }

    fun smartRecoveryStrategy(context: Context): String = when {
        !isBazaarInstalled(context) -> "Bazaar not installed. Please install it first."
        !hasInternetConnection(context) -> "No internet connection. Please connect and try again."
        needsUpdate() -> "Your Bazaar version is outdated. Please update now."
        else -> "Unknown issue. Try restarting your device or reinstalling Bazaar."
    }

    fun toAnalyticsBundle(): Map<String, String> = mapOf(
        "exception" to "BazaarNotSupportedException",
        "package" to packageName,
        "required_version" to (requiredVersion?.toString() ?: "unknown"),
        "current_version" to (currentVersion?.toString() ?: "unknown"),
        "elapsed_seconds" to elapsedSinceThrown().toString(),
        "device" to "${Build.MANUFACTURER} ${Build.MODEL}",
        "android_version" to Build.VERSION.RELEASE
    )

    fun compactSummary(): String =
        "[$packageName] Bazaar outdated (req=$requiredVersion, cur=$currentVersion)"

    fun getErrorId(): String = UUID.randomUUID().toString().substring(0, 8)

    fun diagnosticReport(): String = buildString {
        appendLine(toDebugString())
        appendLine(describe())
        appendLine("Elapsed: ${getReadableElapsedTime()}")
    }

    fun toDebugString(): String {
        val time = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(timestamp))
        return "[BazaarNotSupported] ($time) ${message ?: "No message"}"
    }

    override fun toString(): String =
        "BazaarNotSupportedException(packageName=$packageName, requiredVersion=$requiredVersion, currentVersion=$currentVersion, message=$message, timestamp=$timestamp)"
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import kotlinx.coroutines.delay
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.pow

/**
 * Thrown when a "consume" request to Bazaar fails,
 * typically due to a network, billing, or remote service issue.
 */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String?
        get() = reason ?: "Consume request failed: It's from Bazaar"

    companion object {
        private const val TAG = "ConsumeFailedException"

        /**
         * Creates a preconfigured exception for a known common cause.
         */
        fun fromNetworkError(): ConsumeFailedException {
            return ConsumeFailedException(reason = "Network connection error")
        }

        fun fromInvalidToken(token: String): ConsumeFailedException {
            return ConsumeFailedException(
                purchaseToken = token,
                reason = "Invalid or expired purchase token"
            )
        }

        fun fromTimeout(): ConsumeFailedException {
            return ConsumeFailedException(reason = "Consume request timed out")
        }

        fun fromUnknownError(): ConsumeFailedException {
            return ConsumeFailedException(reason = "Unknown internal error")
        }
    }

    // ===============================================================
    // 📋 Diagnostic & Logging Tools
    // ===============================================================

    fun describe(): String {
        val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
            .format(Date(timestamp))
        return buildString {
            appendLine("⚠️ ConsumeFailedException")
            appendLine("Time       : $date")
            appendLine("Product ID : ${productId ?: "Unknown"}")
            appendLine("Token      : ${purchaseToken ?: "Unknown"}")
            appendLine("Reason     : ${reason ?: "Unknown failure"}")
            appendLine("Message    : ${message}")
        }
    }

    fun log(tag: String = TAG, detailed: Boolean = false) {
        Log.e(tag, "❌ ConsumeFailedException: ${message}")
        if (detailed) {
            Log.e(tag, describe())
        } else {
            if (productId != null) Log.e(tag, "• Product ID: $productId")
            if (purchaseToken != null) Log.e(tag, "• Token: $purchaseToken")
            Log.e(tag, "• Timestamp: $timestamp")
        }
    }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply {
            put("error", "ConsumeFailedException")
            put("message", message)
            put("productId", productId)
            put("purchaseToken", purchaseToken)
            put("reason", reason)
            put("timestamp", timestamp)
            put("retryCount", retryCount)
            put("elapsedSeconds", timeSinceFirstFailure())
        }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "ConsumeFailedException",
        "message" to message,
        "productId" to productId,
        "purchaseToken" to purchaseToken,
        "reason" to reason,
        "timestamp" to timestamp,
        "retryCount" to retryCount,
        "elapsedSeconds" to timeSinceFirstFailure()
    )

    // ===============================================================
    // 🎨 User Feedback Helpers
    // ===============================================================

    /**
     * Safely show a toast on main thread.
     */
    fun showAsToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
        val msg = message ?: "Consume operation failed."
        runOnMain {
            Toast.makeText(context.applicationContext, msg, duration).show()
        }
    }

    fun getReadableError(): String {
        return when {
            reason?.contains("network", true) == true ->
                "Network issue detected. Please check your connection."
            reason?.contains("timeout", true) == true ->
                "The operation took too long. Please try again."
            reason?.contains("token", true) == true ->
                "Purchase token invalid. Please refresh your purchase list."
            else -> "Unable to complete purchase consumption. Try again later."
        }
    }

    fun showFriendlyToast(context: Context, duration: Int = Toast.LENGTH_LONG) {
        runOnMain {
            Toast.makeText(context.applicationContext, getReadableError(), duration).show()
        }
    }

    /**
     * Shows a retry dialog (main thread). Buttons styled using system colors.
     * - onRetry: invoked when user chooses to retry
     * - onCancel: invoked on cancel
     */
    fun showRetryDialog(
        context: Context,
        title: String = "Consume Failed",
        messageText: String = getReadableError(),
        positiveLabel: String = "Retry",
        negativeLabel: String = "Cancel",
        onRetry: (() -> Unit)? = null,
        onCancel: (() -> Unit)? = null
    ) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle(title)
                    .setMessage(messageText)
                    .setCancelable(true)
                    .setPositiveButton(positiveLabel) { dialog, _ ->
                        onRetry?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton(negativeLabel) { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.let {
                        try {
                            it.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                        } catch (_: Exception) { /* ignore */ }
                    }
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.let {
                        try {
                            it.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                        } catch (_: Exception) { /* ignore */ }
                    }
                }
                dialog.show()
            } catch (e: Exception) {
                Log.w(TAG, "Dialog failed: ${e.message}")
                showFriendlyToast(context)
            }
        }
    }

    // ===============================================================
    // ⏱ Timing & Retry Utilities
    // ===============================================================

    // protected by 'synchronized' on methods that mutate them
    private var retryCount: Int = 0
    private var firstFailureTime: Long = SystemClock.elapsedRealtime()

    @synchronized
    fun incrementRetry() {
        retryCount++
    }

    @synchronized
    fun getRetryCount(): Int = retryCount

    @synchronized
    fun resetRetryTracking() {
        retryCount = 0
        firstFailureTime = SystemClock.elapsedRealtime()
    }

    fun timeSinceFirstFailure(): Long =
        (SystemClock.elapsedRealtime() - firstFailureTime) / 1000

    fun getReadableElapsedTime(): String {
        val seconds = timeSinceFirstFailure()
        val minutes = seconds / 60
        val remaining = seconds % 60
        return if (minutes > 0) "${minutes}m ${remaining}s" else "${remaining}s"
    }

    // ===============================================================
    // 🩺 Recovery & Resilience Helpers
    // ===============================================================

    /**
     * Suggests a recovery action based on the cause.
     */
    fun getRecoverySuggestion(): String {
        return when {
            reason?.contains("network", true) == true ->
                "Please ensure your device is connected to the internet."
            reason?.contains("token", true) == true ->
                "The purchase token may have expired. Requery or refresh the purchase list."
            reason?.contains("timeout", true) == true ->
                "Try increasing timeout or retrying later."
            else -> "Try restarting the Bazaar app and retry the operation."
        }
    }

    /**
     * Returns `true` if this failure is likely temporary and worth retrying.
     */
    fun isTemporary(): Boolean {
        return reason?.contains("network", true) == true ||
                reason?.contains("timeout", true) == true
    }

    /**
     * Returns `true` if this failure is likely permanent (e.g., invalid token).
     */
    fun isPermanent(): Boolean {
        return reason?.contains("token", true) == true ||
                reason?.contains("invalid", true) == true
    }

    /**
     * Suggests whether a retry should be attempted.
     */
    fun shouldRetry(maxRetries: Int = 3): Boolean {
        return isTemporary() && getRetryCount() < maxRetries
    }

    // ===============================================================
    // 📊 Analytics & Summary
    // ===============================================================

    fun toAnalyticsBundle(): Map<String, String> = mapOf(
        "exception" to "ConsumeFailedException",
        "product_id" to (productId ?: "unknown"),
        "reason" to (reason ?: "unknown"),
        "elapsed_seconds" to timeSinceFirstFailure().toString(),
        "timestamp" to timestamp.toString(),
        "retries" to getRetryCount().toString()
    )

    fun compactSummary(): String =
        "ConsumeFailed(product=${productId ?: "?"}, reason=${reason ?: "unknown"}, retries=${getRetryCount()})"

    fun diagnosticReport(): String = buildString {
        appendLine("==== Consume Failure Diagnostic ====")
        appendLine(describe())
        appendLine("Retry Count : ${getRetryCount()}")
        appendLine("Elapsed Time: ${getReadableElapsedTime()}")
        appendLine("Temporary   : ${isTemporary()}")
        appendLine("Permanent   : ${isPermanent()}")
        appendLine("Suggestion  : ${getRecoverySuggestion()}")
        appendLine("====================================")
    }

    // ===============================================================
    // 🧰 Utility
    // ===============================================================

    /**
     * Converts this exception into a human-readable log block.
     */
    fun prettyPrint(): String {
        return """
            |🚨 ConsumeFailedException
            |Product ID : ${productId ?: "N/A"}
            |Reason     : ${reason ?: "Unknown"}
            |Retries    : ${getRetryCount()}
            |Temporary  : ${isTemporary()}
            |Timestamp  : $timestamp
            |Message    : $message
        """.trimMargin()
    }

    /**
     * Emits this exception as a standardized debug message.
     */
    fun debugLog(tag: String = TAG) {
        Log.w(tag, prettyPrint())
    }

    // ===============================================================
    // 🔁 Async Retry Helper (suspend) with exponential backoff
    // ===============================================================
    /**
     * Attempts the provided suspend [action] repeatedly with exponential backoff until it returns true
     * or until [maxAttempts] is reached. Returns the success boolean and increments retry counter.
     *
     * Example usage:
     * ```
     * val success = consumeFailedException.retryWithBackoff({
     *     // attempt retry logic (suspend) -> Boolean
     * }, maxAttempts = 4, baseDelayMs = 500L)
     * ```
     */
    suspend fun retryWithBackoff(
        action: suspend () -> Boolean,
        maxAttempts: Int = 4,
        baseDelayMs: Long = 500L
    ): Boolean {
        resetRetryTracking()
        repeat(maxAttempts) { attempt ->
            val ok = try {
                action()
            } catch (e: Exception) {
                false
            }
            if (ok) {
                return true
            } else {
                incrementRetry()
                // exponential backoff (2^attempt * baseDelay)
                val delayMs = baseDelayMs * (2.0.pow(attempt.toDouble())).toLong()
                delay(delayMs)
            }
        }
        return false
    }

    // ===============================================================
    // Helper: post to main looper safely
    // ===============================================================
    private fun runOnMain(runnable: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post { runnable() }
    }

    override fun toString(): String {
        return "ConsumeFailedException(productId=$productId, purchaseToken=$purchaseToken, reason=$reason, message=$message, timestamp=$timestamp)"
    }
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*

/* ===============================================================
   1️⃣ BazaarNotSupportedException
   =============================================================== */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String?
        get() = "Bazaar is not updated"

    companion object {
        private const val TAG = "BazaarNotSupportedException"
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"

        fun getUpdateUri(): Uri = Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
        fun getWebUpdateUri(): Uri = Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")

        fun isBazaarInstalled(context: Context): Boolean =
            try { context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0); true } catch (e: Exception) { false }

        fun getInstalledVersionCode(context: Context): Long? =
            try {
                val info = context.packageManager.getPackageInfo(BAZAAR_PACKAGE, 0)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else @Suppress("DEPRECATION") info.versionCode.toLong()
            } catch (e: Exception) { null }

        fun isVersionSupported(context: Context, requiredVersion: Int): Boolean {
            val current = getInstalledVersionCode(context) ?: return false
            return current >= requiredVersion
        }
    }

    fun showAsToast(context: Context) {
        runOnMain { Toast.makeText(context, "⚠️ ${getRecoveryHint()}", Toast.LENGTH_LONG).show() }
    }

    fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
        val uiAction = {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Update Required")
                    .setMessage("Your Bazaar app is outdated.\n\n${getRecoveryHint()}")
                    .setCancelable(false)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context); onUpdate?.invoke(); dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ -> onCancel?.invoke(); dialog.dismiss() }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                Log.w(TAG, "Dialog failed: ${e.message}")
                showAsToast(context)
            }
        }
        runOnMain(uiAction)
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            val intent = Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
            context.startActivity(intent)
        } catch (e: ActivityNotFoundException) {
            try {
                val webIntent = Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
                context.startActivity(webIntent)
            } catch (ex: Exception) {
                Log.e(TAG, "Failed to open web update page: ${ex.message}")
                showAsToast(context)
            }
        }
    }

    fun getRecoveryHint(): String = recoveryHint ?: "Please update Bazaar to continue."
    fun runOnMain(runnable: () -> Unit) { if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable) }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply {
            put("error", "BazaarNotSupportedException")
            put("package", packageName)
            put("requiredVersion", requiredVersion)
            put("currentVersion", currentVersion)
            put("timestamp", timestamp)
            put("hint", recoveryHint)
        }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "BazaarNotSupportedException",
        "package" to packageName,
        "requiredVersion" to requiredVersion,
        "currentVersion" to currentVersion,
        "timestamp" to timestamp,
        "hint" to recoveryHint
    )
}

/* ===============================================================
   2️⃣ ConsumeFailedException
   =============================================================== */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String? get() = reason ?: "Consume request failed: It's from Bazaar"

    companion object {
        fun fromNetworkError() = ConsumeFailedException(reason = "Network connection error")
        fun fromInvalidToken(token: String) = ConsumeFailedException(purchaseToken = token, reason = "Invalid or expired purchase token")
        fun fromTimeout() = ConsumeFailedException(reason = "Consume request timed out")
        fun fromUnknownError() = ConsumeFailedException(reason = "Unknown internal error")
    }

    private var retryCount: Int = 0
    private var firstFailureTime: Long = SystemClock.elapsedRealtime()

    fun incrementRetry() { retryCount++ }
    fun getRetryCount(): Int = retryCount
    fun resetRetryTracking() { retryCount = 0; firstFailureTime = SystemClock.elapsedRealtime() }

    fun isTemporary(): Boolean = reason?.contains("network", true) == true || reason?.contains("timeout", true) == true
    fun isPermanent(): Boolean = reason?.contains("token", true) == true || reason?.contains("invalid", true) == true
    fun shouldRetry(maxRetries: Int = 3): Boolean = isTemporary() && retryCount < maxRetries

    fun getRecoverySuggestion(): String = when {
        reason?.contains("network", true) == true -> "Check your internet connection."
        reason?.contains("token", true) == true -> "Purchase token may have expired."
        reason?.contains("timeout", true) == true -> "Try increasing timeout or retrying later."
        else -> "Restart Bazaar and retry the operation."
    }

    fun showFriendlyToast(context: Context) { runOnMain { Toast.makeText(context, "⚠️ ${getRecoverySuggestion()}", Toast.LENGTH_LONG).show() } }
    private fun runOnMain(runnable: () -> Unit) { if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable) }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply {
            put("error", "ConsumeFailedException")
            put("productId", productId)
            put("purchaseToken", purchaseToken)
            put("reason", reason)
            put("timestamp", timestamp)
            put("retryCount", retryCount)
            put("elapsedSeconds", (SystemClock.elapsedRealtime() - firstFailureTime)/1000)
        }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "ConsumeFailedException",
        "productId" to productId,
        "purchaseToken" to purchaseToken,
        "reason" to reason,
        "timestamp" to timestamp,
        "retryCount" to retryCount
    )
}

/* ===============================================================
   3️⃣ DisconnectException
   =============================================================== */
class DisconnectException : IllegalStateException() {

    override val message: String? get() = "We can't communicate with Bazaar: Service is disconnected"

    fun showAsToast(context: Context) { Toast.makeText(context, "⚠️ $message", Toast.LENGTH_LONG).show() }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply { put("error", "DisconnectException"); put("message", message) }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun toMap(): Map<String, Any?> = mapOf("type" to "DisconnectException", "message" to message)
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*

/* ===============================================================
   1️⃣ BazaarNotSupportedException
   =============================================================== */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String
        get() = "Bazaar is not updated"

    companion object {
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"

        fun getUpdateUri(): android.net.Uri = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
        fun getWebUpdateUri(): android.net.Uri = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
    }

    fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion

    fun elapsedSinceThrown(): Long = (System.currentTimeMillis() - timestamp) / 1000

    fun getReadableElapsedTime(): String {
        val seconds = elapsedSinceThrown()
        val minutes = seconds / 60
        val remaining = seconds % 60
        return if (minutes > 0) "${minutes}m ${remaining}s ago" else "${remaining}s ago"
    }

    fun hasInternetConnection(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network) ?: return false
        return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }

    fun showAsToast(context: Context) {
        runOnMain { Toast.makeText(context, getRecoveryHint(), Toast.LENGTH_LONG).show() }
    }

    fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Update Required")
                    .setMessage(getRecoveryHint())
                    .setCancelable(false)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context)
                        onUpdate?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                showAsToast(context)
            }
        }
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            val intent = Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
            context.startActivity(intent)
        } catch (e: ActivityNotFoundException) {
            try {
                val webIntent = Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
                context.startActivity(webIntent)
            } catch (_: Exception) {
                showAsToast(context)
            }
        }
    }

    fun getRecoveryHint(): String = recoveryHint ?: "Please update Bazaar to continue."

    fun diagnosticReport(context: Context): String = buildString {
        appendLine("=== BazaarNotSupportedException Diagnostic ===")
        appendLine("Package         : $packageName")
        appendLine("Required Version: ${requiredVersion ?: "Unknown"}")
        appendLine("Current Version : ${currentVersion ?: "Unknown"}")
        appendLine("Hint            : ${getRecoveryHint()}")
        appendLine("Elapsed Time    : ${getReadableElapsedTime()}")
        appendLine("Internet        : ${if (hasInternetConnection(context)) "Available" else "Unavailable"}")
        appendLine("==============================================")
    }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply {
            put("error", "BazaarNotSupportedException")
            put("package", packageName)
            put("requiredVersion", requiredVersion)
            put("currentVersion", currentVersion)
            put("timestamp", timestamp)
            put("hint", getRecoveryHint())
        }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "BazaarNotSupportedException",
        "package" to packageName,
        "requiredVersion" to requiredVersion,
        "currentVersion" to currentVersion,
        "timestamp" to timestamp,
        "hint" to getRecoveryHint()
    )

    private fun runOnMain(runnable: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
    }
}

/* ===============================================================
   2️⃣ ConsumeFailedException
   =============================================================== */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String
        get() = reason ?: "Consume request failed: It's from Bazaar"

    private var retryCount: Int = 0
    private var firstFailureTime: Long = SystemClock.elapsedRealtime()

    fun incrementRetry() { retryCount++ }
    fun getRetryCount(): Int = retryCount
    fun resetRetryTracking() { retryCount = 0; firstFailureTime = SystemClock.elapsedRealtime() }

    fun timeSinceFirstFailureSeconds(): Long = (SystemClock.elapsedRealtime() - firstFailureTime) / 1000

    fun getReadableTimeSinceFailure(): String {
        val seconds = timeSinceFirstFailureSeconds()
        val minutes = seconds / 60
        val remaining = seconds % 60
        return if (minutes > 0) "${minutes}m ${remaining}s ago" else "${remaining}s ago"
    }

    fun isTemporary(): Boolean = reason?.contains("network", true) == true || reason?.contains("timeout", true) == true
    fun isPermanent(): Boolean = reason?.contains("token", true) == true || reason?.contains("invalid", true) == true
    fun isRetryable(maxRetries: Int = 3): Boolean = isTemporary() && retryCount < maxRetries

    fun getRecoverySuggestion(): String = when {
        reason?.contains("network", true) == true -> "Check your internet connection."
        reason?.contains("token", true) == true -> "Purchase token may have expired."
        reason?.contains("timeout", true) == true -> "Try increasing timeout or retrying later."
        else -> "Restart Bazaar and retry the operation."
    }

    fun showFriendlyToast(context: Context) {
        runOnMain { Toast.makeText(context, getRecoverySuggestion(), Toast.LENGTH_LONG).show() }
    }

    fun fullDiagnosticReport(): String = buildString {
        appendLine("=== ConsumeFailedException Diagnostic ===")
        appendLine("Product ID      : ${productId ?: "Unknown"}")
        appendLine("Purchase Token  : ${purchaseToken ?: "Unknown"}")
        appendLine("Reason          : ${reason ?: "Unknown"}")
        appendLine("Retry Count     : $retryCount")
        appendLine("Elapsed Time    : ${getReadableTimeSinceFailure()}")
        appendLine("Temporary       : ${isTemporary()}")
        appendLine("Permanent       : ${isPermanent()}")
        appendLine("Recovery Hint   : ${getRecoverySuggestion()}")
        appendLine("=========================================")
    }

    fun toJson(pretty: Boolean = true): String {
        val obj = JSONObject().apply {
            put("error", "ConsumeFailedException")
            put("productId", productId)
            put("purchaseToken", purchaseToken)
            put("reason", reason)
            put("timestamp", timestamp)
            put("retryCount", retryCount)
            put("elapsedSeconds", timeSinceFirstFailureSeconds())
        }
        return if (pretty) obj.toString(2) else obj.toString()
    }

    fun toMap(): Map<String, Any?> = mapOf(
        "type" to "ConsumeFailedException",
        "productId" to productId,
        "purchaseToken" to purchaseToken,
        "reason" to reason,
        "timestamp" to timestamp,
        "retryCount" to retryCount
    )

    private fun runOnMain(runnable: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
    }

    companion object {
        fun fromNetworkError() = ConsumeFailedException(reason = "Network connection error")
        fun fromInvalidToken(token: String) = ConsumeFailedException(purchaseToken = token, reason = "Invalid or expired purchase token")
        fun fromTimeout() = ConsumeFailedException(reason = "Consume request timed out")
        fun fromUnknownError() = ConsumeFailedException(reason = "Unknown internal error")
    }
}

/* ===============================================================
   3️⃣ DisconnectException
   =============================================================== */
class DisconnectException : IllegalStateException() {

    override val message: String
        get() = "We can't communicate with Bazaar: Service is disconnected"

    fun showAsToast(context: Context) {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }

    fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Service Disconnected")
                    .setMessage(message)
                    .setCancelable(false)
                    .setPositiveButton("Retry 🔄") { dialog, _ ->
                        onRetry?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Dismiss ❌") { dialog, _ ->
                        dialog.dismiss()
                    }
                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_blue_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                showAsToast(context)
            }
        }
    }

    fun isRecoverable(): Boolean = true

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "DisconnectException")
        put("message", message)
    }.let { if (pretty) it.toString(2) else it.toString() }

    fun toMap(): Map<String, Any?> = mapOf("type" to "DisconnectException", "message" to message)

    fun toAnalyticsMap(): Map<String, String> = mapOf("type" to "DisconnectException", "message" to message, "timestamp" to System.currentTimeMillis().toString())

    private fun runOnMain(runnable: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
    }
}

/* ===============================================================
   4️⃣ DynamicPriceNotSupportedException
   =============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {

    override val message: String
        get() = "Dynamic price not supported"

    fun showDialog(context: Context) {
        runOnMain {
            try {
                AlertDialog.Builder(context)
                    .setTitle("⚠️ Unsupported Feature")
                    .setMessage(message)
                    .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
                    .show()
            } catch (e: Exception) {
                Toast.makeText(context, message, Toast.LENGTH_LONG).show()
            }
        }
    }

    fun toAnalyticsMap(): Map<String, String> = mapOf("type" to "DynamicPriceNotSupportedException", "message" to message, "timestamp" to System.currentTimeMillis().toString())

    private fun runOnMain(runnable: () -> Unit) {
        if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
    }
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject

private fun runOnMain(runnable: () -> Unit) {
    if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}

/* ===============================================================
   1️⃣ BazaarNotSupportedException
   =============================================================== */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String get() = "Bazaar is not updated"

    companion object {
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
        fun getUpdateUri(): android.net.Uri = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
        fun getWebUpdateUri(): android.net.Uri = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
    }

    fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion

    fun hasInternetConnection(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network) ?: return false
        return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }

    fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Update Required")
                    .setMessage(recoveryHint)
                    .setCancelable(false)
                    .setIcon(android.R.drawable.ic_dialog_alert)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context)
                        onUpdate?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                showAsToast(context)
            }
        }
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
        } catch (e: ActivityNotFoundException) {
            try {
                context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
            } catch (_: Exception) {
                showAsToast(context)
            }
        }
    }

    fun showAsToast(context: Context) = runOnMain { Toast.makeText(context, recoveryHint, Toast.LENGTH_LONG).show() }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "BazaarNotSupportedException")
        put("package", packageName)
        put("requiredVersion", requiredVersion)
        put("currentVersion", currentVersion)
        put("timestamp", timestamp)
        put("hint", recoveryHint)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   2️⃣ ConsumeFailedException
   =============================================================== */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String get() = reason ?: "Consume request failed"

    private var retryCount = 0
    private var firstFailureTime = SystemClock.elapsedRealtime()

    fun incrementRetry() { retryCount++ }
    fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries

    fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
        if (isRetryable(maxRetries)) {
            incrementRetry()
            runOnMain { Handler().postDelayed({ operation() }, 2000) } // Non-blocking
        }
    }

    fun showFriendlyToast(context: Context) = runOnMain { Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show() }

    fun recoverySuggestion(): String = when {
        reason?.contains("network", true) == true -> "Check your internet connection."
        reason?.contains("token", true) == true -> "Purchase token may have expired."
        reason?.contains("timeout", true) == true -> "Try again later."
        else -> "Restart Bazaar and retry."
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "ConsumeFailedException")
        put("productId", productId)
        put("purchaseToken", purchaseToken)
        put("reason", reason)
        put("timestamp", timestamp)
        put("retryCount", retryCount)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   3️⃣ DisconnectException
   =============================================================== */
class DisconnectException : IllegalStateException() {

    override val message: String get() = "Bazaar service disconnected"

    fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Service Disconnected")
                .setMessage(message)
                .setCancelable(false)
                .setIcon(android.R.drawable.ic_dialog_alert)
                .setPositiveButton("Retry 🔄") { dialog, _ ->
                    onRetry?.invoke()
                    dialog.dismiss()
                }
                .setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
                .show()
        }
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "DisconnectException")
        put("message", message)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   4️⃣ DynamicPriceNotSupportedException
   =============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
    override val message: String get() = "Dynamic pricing not supported"

    fun showDialog(context: Context) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Unsupported Feature")
                .setMessage(message)
                .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
                .setIcon(android.R.drawable.ic_dialog_info)
                .show()
        }
    }
}

/* ===============================================================
   5️⃣ IAPNotSupportedException
   =============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
    override val message: String? get() = "In-app billing not supported"

    fun notifyUser(context: Context) = runOnMain { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.lang.Exception

// Centralized utility function to run code on the main thread
private fun runOnMain(runnable: () -> Unit) {
    if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}

/* ===============================================================
   1️⃣ BazaarNotSupportedException
   =============================================================== */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String get() = "Bazaar is not updated"

    companion object {
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
        fun getUpdateUri() = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
        fun getWebUpdateUri() = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
    }

    fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion

    fun hasInternetConnection(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network) ?: return false
        return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }

    fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Update Required")
                    .setMessage(recoveryHint)
                    .setCancelable(false)
                    .setIcon(android.R.drawable.ic_dialog_alert)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context)
                        onUpdate?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                showAsToast(context)
            }
        }
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
        } catch (e: ActivityNotFoundException) {
            try {
                context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
            } catch (_: Exception) {
                showAsToast(context)
            }
        }
    }

    fun showAsToast(context: Context) = runOnMain { Toast.makeText(context, recoveryHint ?: "Update Bazaar to continue", Toast.LENGTH_LONG).show() }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "BazaarNotSupportedException")
        put("package", packageName)
        put("requiredVersion", requiredVersion)
        put("currentVersion", currentVersion)
        put("timestamp", timestamp)
        put("hint", recoveryHint)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   2️⃣ ConsumeFailedException
   =============================================================== */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String get() = reason ?: "Consume request failed"

    private var retryCount = 0
    private var firstFailureTime = SystemClock.elapsedRealtime()

    fun incrementRetry() { retryCount++ }
    fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries

    fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
        if (isRetryable(maxRetries)) {
            incrementRetry()
            runOnMain { Handler().postDelayed({ operation() }, 2000) } // Non-blocking retry
        }
    }

    fun showFriendlyToast(context: Context) = runOnMain { Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show() }

    fun recoverySuggestion(): String = when {
        reason?.contains("network", true) == true -> "Check your internet connection."
        reason?.contains("token", true) == true -> "Purchase token may have expired."
        reason?.contains("timeout", true) == true -> "Try again later."
        else -> "Restart Bazaar and retry."
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "ConsumeFailedException")
        put("productId", productId)
        put("purchaseToken", purchaseToken)
        put("reason", reason)
        put("timestamp", timestamp)
        put("retryCount", retryCount)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   3️⃣ DisconnectException
   =============================================================== */
class DisconnectException : IllegalStateException() {
    override val message: String get() = "Bazaar service disconnected"

    fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Service Disconnected")
                .setMessage(message)
                .setCancelable(false)
                .setIcon(android.R.drawable.ic_dialog_alert)
                .setPositiveButton("Retry 🔄") { dialog, _ ->
                    onRetry?.invoke()
                    dialog.dismiss()
                }
                .setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
                .show()
        }
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "DisconnectException")
        put("message", message)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   4️⃣ DynamicPriceNotSupportedException
   =============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
    override val message: String get() = "Dynamic pricing not supported"

    fun showDialog(context: Context) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Unsupported Feature")
                .setMessage(message)
                .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
                .setIcon(android.R.drawable.ic_dialog_info)
                .show()
        }
    }
}

/* ===============================================================
   5️⃣ IAPNotSupportedException
   =============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
    override val message: String? get() = "In-app billing not supported"

    fun notifyUser(context: Context) = runOnMain { Toast.makeText(context, message, Toast.LENGTH_LONG).show() }
}

/* ===============================================================
   6️⃣ PurchaseHijackedException
   =============================================================== */
class PurchaseHijackedException : Exception() {
    override val message: String? get() = "The purchase was hijacked and it's not a valid purchase"
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.lang.Exception

// Run safely on main thread
private fun runOnMain(runnable: () -> Unit) {
    if (Looper.myLooper() == Looper.getMainLooper()) runnable()
    else Handler(Looper.getMainLooper()).post(runnable)
}

/* ===============================================================
   1️⃣ BazaarNotSupportedException
   =============================================================== */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String get() = "Bazaar is not updated"

    companion object {
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
        fun getUpdateUri() = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
        fun getWebUpdateUri() = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
    }

    fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion

    fun hasInternetConnection(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network) ?: return false
        return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }

    fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Update Required")
                    .setMessage(recoveryHint)
                    .setCancelable(false)
                    .setIcon(android.R.drawable.ic_dialog_alert)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context)
                        onUpdate?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                showAsToast(context)
            }
        }
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
        } catch (e: ActivityNotFoundException) {
            try {
                context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
            } catch (_: Exception) {
                showAsToast(context)
            }
        }
    }

    fun showAsToast(context: Context) = runOnMain { 
        Toast.makeText(context, recoveryHint ?: "Update Bazaar to continue", Toast.LENGTH_LONG).show() 
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "BazaarNotSupportedException")
        put("package", packageName)
        put("requiredVersion", requiredVersion)
        put("currentVersion", currentVersion)
        put("timestamp", timestamp)
        put("hint", recoveryHint)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   2️⃣ ConsumeFailedException
   =============================================================== */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String get() = reason ?: "Consume request failed"

    private var retryCount = 0
    private var firstFailureTime = SystemClock.elapsedRealtime()

    fun incrementRetry() { retryCount++ }
    fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries

    fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
        if (isRetryable(maxRetries)) {
            incrementRetry()
            runOnMain { Handler().postDelayed({ operation() }, 2000) }
        }
    }

    fun showFriendlyToast(context: Context) = runOnMain { 
        Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show() 
    }

    fun recoverySuggestion(): String = when {
        reason?.contains("network", true) == true -> "Check your internet connection."
        reason?.contains("token", true) == true -> "Purchase token may have expired."
        reason?.contains("timeout", true) == true -> "Try again later."
        else -> "Restart Bazaar and retry."
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "ConsumeFailedException")
        put("productId", productId)
        put("purchaseToken", purchaseToken)
        put("reason", reason)
        put("timestamp", timestamp)
        put("retryCount", retryCount)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   3️⃣ DisconnectException
   =============================================================== */
class DisconnectException : IllegalStateException() {
    override val message: String get() = "Bazaar service disconnected"

    fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Service Disconnected")
                .setMessage(message)
                .setCancelable(false)
                .setIcon(android.R.drawable.ic_dialog_alert)
                .setPositiveButton("Retry 🔄") { dialog, _ ->
                    onRetry?.invoke()
                    dialog.dismiss()
                }
                .setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
                .show()
        }
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "DisconnectException")
        put("message", message)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   4️⃣ DynamicPriceNotSupportedException
   =============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
    override val message: String get() = "Dynamic pricing not supported"

    fun showDialog(context: Context) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Unsupported Feature")
                .setMessage(message)
                .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
                .setIcon(android.R.drawable.ic_dialog_info)
                .show()
        }
    }
}

/* ===============================================================
   5️⃣ IAPNotSupportedException
   =============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
    override val message: String? get() = "In-app billing not supported"

    fun notifyUser(context: Context) = runOnMain { 
        Toast.makeText(context, message, Toast.LENGTH_LONG).show() 
    }
}

/* ===============================================================
   6️⃣ PurchaseHijackedException
   =============================================================== */
class PurchaseHijackedException : Exception() {
    override val message: String? get() = "The purchase was hijacked and it's not a valid purchase"
}

/* ===============================================================
   7️⃣ ResultNotOkayException
   =============================================================== */
class ResultNotOkayException : IllegalStateException() {
    override val message: String? get() = "Failed to receive response from Bazaar"
}
package ir.cafebazaar.poolakey.exception

import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
import android.os.SystemClock
import android.widget.Toast
import androidx.core.content.ContextCompat
import org.json.JSONObject
import java.lang.Exception

// Centralized utility function to run code on the main thread
private fun runOnMain(runnable: () -> Unit) {
    if (Looper.myLooper() == Looper.getMainLooper()) runnable() else Handler(Looper.getMainLooper()).post(runnable)
}

/* ===============================================================
   1️⃣ BazaarNotSupportedException
   =============================================================== */
class BazaarNotSupportedException(
    val packageName: String = "com.farsitel.bazaar",
    val requiredVersion: Int? = null,
    val currentVersion: Int? = null,
    val timestamp: Long = System.currentTimeMillis(),
    val recoveryHint: String? = "Please update Bazaar to the latest version to continue."
) : IllegalStateException() {

    override val message: String get() = "Bazaar is not updated"

    companion object {
        private const val BAZAAR_PACKAGE = "com.farsitel.bazaar"
        fun getUpdateUri() = android.net.Uri.parse("bazaar://details?id=$BAZAAR_PACKAGE")
        fun getWebUpdateUri() = android.net.Uri.parse("https://cafebazaar.ir/app/$BAZAAR_PACKAGE?l=en")
    }

    fun needsUpdate(): Boolean = requiredVersion != null && currentVersion != null && currentVersion < requiredVersion

    fun hasInternetConnection(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false
        val network = cm.activeNetwork ?: return false
        val caps = cm.getNetworkCapabilities(network) ?: return false
        return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }

    fun showUpdateDialog(context: Context, onUpdate: (() -> Unit)? = null, onCancel: (() -> Unit)? = null) {
        runOnMain {
            try {
                val builder = AlertDialog.Builder(context)
                    .setTitle("⚠️ Bazaar Update Required")
                    .setMessage(recoveryHint)
                    .setCancelable(false)
                    .setIcon(android.R.drawable.ic_dialog_alert)
                    .setPositiveButton("Update Now 🚀") { dialog, _ ->
                        openBazaarUpdatePage(context)
                        onUpdate?.invoke()
                        dialog.dismiss()
                    }
                    .setNegativeButton("Cancel ❌") { dialog, _ ->
                        onCancel?.invoke()
                        dialog.dismiss()
                    }

                val dialog = builder.create()
                dialog.setOnShowListener {
                    dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_green_dark))
                    dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ContextCompat.getColor(context, android.R.color.holo_red_dark))
                }
                dialog.show()
            } catch (e: Exception) {
                showAsToast(context)
            }
        }
    }

    fun openBazaarUpdatePage(context: Context) {
        try {
            context.startActivity(Intent(Intent.ACTION_VIEW, getUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
        } catch (e: ActivityNotFoundException) {
            try {
                context.startActivity(Intent(Intent.ACTION_VIEW, getWebUpdateUri()).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) })
            } catch (_: Exception) {
                showAsToast(context)
            }
        }
    }

    fun showAsToast(context: Context) = runOnMain {
        Toast.makeText(context, recoveryHint ?: "Update Bazaar to continue", Toast.LENGTH_LONG).show()
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "BazaarNotSupportedException")
        put("package", packageName)
        put("requiredVersion", requiredVersion)
        put("currentVersion", currentVersion)
        put("timestamp", timestamp)
        put("hint", recoveryHint)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   2️⃣ ConsumeFailedException
   =============================================================== */
class ConsumeFailedException(
    val productId: String? = null,
    val purchaseToken: String? = null,
    val reason: String? = null,
    val timestamp: Long = System.currentTimeMillis()
) : RemoteException() {

    override val message: String get() = reason ?: "Consume request failed"

    private var retryCount = 0
    private var firstFailureTime = SystemClock.elapsedRealtime()

    fun incrementRetry() { retryCount++ }
    fun isRetryable(maxRetries: Int = 3) = reason?.contains("network", true) == true && retryCount < maxRetries

    fun retryOperationIfNeeded(maxRetries: Int = 3, operation: () -> Unit) {
        if (isRetryable(maxRetries)) {
            incrementRetry()
            runOnMain { Handler().postDelayed({ operation() }, 2000) }
        }
    }

    fun showFriendlyToast(context: Context) = runOnMain {
        Toast.makeText(context, recoverySuggestion(), Toast.LENGTH_LONG).show()
    }

    fun recoverySuggestion(): String = when {
        reason?.contains("network", true) == true -> "Check your internet connection."
        reason?.contains("token", true) == true -> "Purchase token may have expired."
        reason?.contains("timeout", true) == true -> "Try again later."
        else -> "Restart Bazaar and retry."
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "ConsumeFailedException")
        put("productId", productId)
        put("purchaseToken", purchaseToken)
        put("reason", reason)
        put("timestamp", timestamp)
        put("retryCount", retryCount)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   3️⃣ DisconnectException
   =============================================================== */
class DisconnectException : IllegalStateException() {
    override val message: String get() = "Bazaar service disconnected"

    fun showEnhancedDialog(context: Context, onRetry: (() -> Unit)? = null) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Service Disconnected")
                .setMessage(message)
                .setCancelable(false)
                .setIcon(android.R.drawable.ic_dialog_alert)
                .setPositiveButton("Retry 🔄") { dialog, _ ->
                    onRetry?.invoke()
                    dialog.dismiss()
                }
                .setNegativeButton("Dismiss ❌") { dialog, _ -> dialog.dismiss() }
                .show()
        }
    }

    fun toJson(pretty: Boolean = true): String = JSONObject().apply {
        put("error", "DisconnectException")
        put("message", message)
    }.let { if (pretty) it.toString(2) else it.toString() }
}

/* ===============================================================
   4️⃣ DynamicPriceNotSupportedException
   =============================================================== */
class DynamicPriceNotSupportedException : IllegalStateException() {
    override val message: String get() = "Dynamic pricing not supported"

    fun showDialog(context: Context) {
        runOnMain {
            AlertDialog.Builder(context)
                .setTitle("⚠️ Unsupported Feature")
                .setMessage(message)
                .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
                .setIcon(android.R.drawable.ic_dialog_info)
                .show()
        }
    }
}

/* ===============================================================
   5️⃣ IAPNotSupportedException
   =============================================================== */
class IAPNotSupportedException : IllegalAccessException() {
    override val message: String? get() = "In-app billing not supported"

    fun notifyUser(context: Context) = runOnMain {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
}

/* ===============================================================
   6️⃣ PurchaseHijackedException
   =============================================================== */
class PurchaseHijackedException : Exception() {
    override val message: String? get() = "The purchase was hijacked and it's not a valid purchase"
}

/* ===============================================================
   7️⃣ ResultNotOkayException
   =============================================================== */
class ResultNotOkayException : IllegalStateException() {
    override val message: String? get() = "Failed to receive response from Bazaar"
}

/* ===============================================================
   8️⃣ SubsNotSupportedException
   =============================================================== */
class SubsNotSupportedException : IllegalAccessException() {
    override val message: String?
        get() = "Subscription is not supported in this version of installed Bazaar"
}
android_exception_manager.py
`InAppPurchaseModels.kt`
subscription_manager.py
`BazaarConstants.kt`
BazaarConstants.py
purchase_analysis.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant