<?php

defined("ABSPATH") || exit();

class Chit_Codes
{
    public const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";

    public const MAX_CODE_LENGTH = 20;
    public const MIN_RANDOM_LENGTH = 8;
    public const GROUP_SIZES = [4, 5, 6];
    public const GROUP_COUNTS = [2, 3, 4];

    public const FORMAT_DEFAULTS = [
        "prefix" => "CHIT",
        "group_size" => 4,
        "groups" => 2,
    ];

    public static function generate_code(): string
    {
        $format = self::get_format();

        $out = "";
        $max = strlen(self::ALPHABET) - 1;
        $length = $format["groups"] * $format["group_size"];
        for ($i = 0; $i < $length; $i++) {
            $out .= self::ALPHABET[random_int(0, $max)];
        }

        return self::apply_prefix(
            self::group($out, $format["group_size"]),
            $format["prefix"]
        );
    }

    /**
     * Sample of the current format with X placeholders, e.g. "CHIT-XXXX-XXXX".
     */
    public static function placeholder(): string
    {
        $format = self::get_format();
        return self::apply_prefix(
            self::group(
                str_repeat("X", $format["groups"] * $format["group_size"]),
                $format["group_size"]
            ),
            $format["prefix"]
        );
    }

    /**
     * The sanitised active code format. Falls back to defaults if the
     * stored option is missing or would produce an over-long code.
     */
    public static function get_format(): array
    {
        $format = self::sanitize_format(
            (array) get_option("chit_code_format", [])
        );
        if (self::format_length($format) > self::MAX_CODE_LENGTH) {
            return self::FORMAT_DEFAULTS;
        }
        return $format;
    }

    /**
     * Coerce raw input into a well-formed array. Does NOT enforce
     * MAX_CODE_LENGTH — callers need to validate via format_length().
     */
    public static function sanitize_format(array $raw): array
    {
        $prefix = strtoupper((string) ($raw["prefix"] ?? self::FORMAT_DEFAULTS["prefix"]));
        $prefix = preg_replace("/[^A-Z0-9-]/", "", $prefix);
        // The prefix is stored bare — the dash separating it from the code
        // body is added at generation time, so trim any the user typed.
        $prefix = trim($prefix, "-");

        $group_size = (int) ($raw["group_size"] ?? self::FORMAT_DEFAULTS["group_size"]);
        if (!in_array($group_size, self::GROUP_SIZES, true)) {
            $group_size = self::FORMAT_DEFAULTS["group_size"];
        }

        $groups = (int) ($raw["groups"] ?? self::FORMAT_DEFAULTS["groups"]);
        if (!in_array($groups, self::GROUP_COUNTS, true)) {
            $groups = self::FORMAT_DEFAULTS["groups"];
        }

        return [
            "prefix" => $prefix,
            "group_size" => $group_size,
            "groups" => $groups,
        ];
    }

    /**
     * Total length of a code with separators included.
     */
    public static function format_length(array $format): int
    {
        $prefix_length =
            $format["prefix"] === "" ? 0 : strlen($format["prefix"]) + 1;
        return $prefix_length +
            $format["groups"] * $format["group_size"] +
            ($format["groups"] - 1);
    }

    private static function apply_prefix(string $body, string $prefix): string
    {
        return $prefix === "" ? $body : $prefix . "-" . $body;
    }

    private static function group(string $chars, int $group_size): string
    {
        if ($group_size <= 0) {
            return $chars;
        }
        return implode("-", str_split($chars, $group_size));
    }

    public static function get_codes(array $args = []): array
    {
        global $wpdb;
        $codes_table = $wpdb->prefix . "chit_codes";
        $batches_table = $wpdb->prefix . "chit_batches";

        $defaults = [
            "status" => null,
            "product_id" => null,
            "batch_id" => null,
            "orderby" => "created_at",
            "order" => "DESC",
            "per_page" => 20,
            "page" => 1,
        ];
        $args = array_merge($defaults, $args);

        [$where, $params] = self::build_where($args, "c");

        $orderby = self::sanitize_orderby($args["orderby"]);
        $order = strtoupper($args["order"]) === "ASC" ? "ASC" : "DESC";
        $per_page = max(1, (int) $args["per_page"]);
        $offset = (max(1, (int) $args["page"]) - 1) * $per_page;

        $sql =
            "SELECT c.*, b.note AS batch_note " .
            "FROM $codes_table c " .
            "LEFT JOIN $batches_table b ON b.id = c.batch_id " .
            "$where ORDER BY c.$orderby $order LIMIT %d OFFSET %d";
        $params[] = $per_page;
        $params[] = $offset;

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql built from $wpdb->prefix table names, sanitize_orderby() whitelist, and prepare() placeholders
        return $wpdb->get_results($wpdb->prepare($sql, ...$params), ARRAY_A);
    }

    public static function get_codes_count(array $args = []): int
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";

        [$where, $params] = self::build_where($args, "c");

        $sql = "SELECT COUNT(*) FROM $table c $where";
        if ($params) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql from $wpdb->prefix and prepare() placeholders
            return (int) $wpdb->get_var($wpdb->prepare($sql, ...$params));
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql from $wpdb->prefix, no user input
        return (int) $wpdb->get_var($sql);
    }

    public static function revoke(int $code_id): bool
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        $result = $wpdb->update(
            $table,
            ["status" => "revoked"],
            ["id" => $code_id, "status" => "unused"],
            ["%s"],
            ["%d", "%s"]
        );
        return $result !== false && $result > 0;
    }

    public static function find_by_code(string $code): ?array
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table from $wpdb->prefix
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM $table WHERE code = %s LIMIT 1",
                $code
            ),
            ARRAY_A
        );
        return $row ?: null;
    }

    public static function activate(int $code_id, ?int $user_id): bool
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        $result = $wpdb->update(
            $table,
            [
                "status" => "activated",
                "activated_at" => current_time("mysql"),
                "activated_by" => $user_id,
            ],
            ["id" => $code_id, "status" => "unused"],
            ["%s", "%s", "%d"],
            ["%d", "%s"]
        );
        return $result !== false && $result > 0;
    }

    public static function activator_label(array $row): string
    {
        // account name or email if guest redemption
        $user_id = (int) ($row["activated_by"] ?? 0);
        if ($user_id) {
            $user = get_userdata($user_id);
            return $user ? $user->display_name : "";
        }
        $order_id = (int) ($row["order_id"] ?? 0);
        if ($order_id && function_exists("wc_get_order")) {
            $order = wc_get_order($order_id);
            if ($order instanceof WC_Order && $order->get_billing_email()) {
                return sprintf(
                    /* translators: %s = email address */
                    __("%s (guest)", "chit-download-codes-for-woocommerce"),
                    $order->get_billing_email()
                );
            }
        }
        return "";
    }

    public static function revert_activation(int $code_id): void
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        $wpdb->update(
            $table,
            [
                "status" => "unused",
                "activated_at" => null,
                "activated_by" => null,
                "order_id" => null,
            ],
            ["id" => $code_id],
            ["%s", "%s", "%d", "%d"],
            ["%d"]
        );
    }

    public static function set_order_id(int $code_id, int $order_id): void
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        $wpdb->update(
            $table,
            ["order_id" => $order_id],
            ["id" => $code_id],
            ["%d"],
            ["%d"]
        );
    }

    public static function get_stats(): array
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table from $wpdb->prefix
        $rows = $wpdb->get_results(
            "SELECT status, COUNT(*) AS count FROM $table GROUP BY status",
            ARRAY_A
        );

        $stats = [
            "total" => 0,
            "unused" => 0,
            "activated" => 0,
            "revoked" => 0,
            "expired" => 0,
        ];
        foreach ($rows as $row) {
            if (isset($stats[$row["status"]])) {
                $stats[$row["status"]] = (int) $row["count"];
            }
            $stats["total"] += (int) $row["count"];
        }
        return $stats;
    }

    public static function get_top_products(int $limit = 10): array
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table from $wpdb->prefix
        return $wpdb->get_results(
            $wpdb->prepare(
                "SELECT
                    product_id,
                    COUNT(*) AS total,
                    SUM(CASE WHEN status = 'activated' THEN 1 ELSE 0 END) AS activated
                FROM $table
                GROUP BY product_id
                ORDER BY total DESC
                LIMIT %d",
                $limit
            ),
            ARRAY_A
        );
    }

    public static function get_recent_activations(int $limit = 5): array
    {
        global $wpdb;
        $table = $wpdb->prefix . "chit_codes";
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $table from $wpdb->prefix
        return $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM $table
                WHERE status = 'activated' AND activated_at IS NOT NULL
                ORDER BY activated_at DESC
                LIMIT %d",
                $limit
            ),
            ARRAY_A
        );
    }

    private static function build_where(array $args, string $alias = ""): array
    {
        $p = $alias ? $alias . "." : "";
        $where = [];
        $params = [];
        if (!empty($args["status"])) {
            $where[] = "{$p}status = %s";
            $params[] = $args["status"];
        }
        if (!empty($args["product_id"])) {
            $where[] = "{$p}product_id = %d";
            $params[] = (int) $args["product_id"];
        }
        if (!empty($args["batch_id"])) {
            $where[] = "{$p}batch_id = %d";
            $params[] = (int) $args["batch_id"];
        }
        return [
            $where ? "WHERE " . implode(" AND ", $where) : "",
            $params,
        ];
    }

    private static function sanitize_orderby(string $field): string
    {
        $allowed = [
            "id",
            "code",
            "status",
            "product_id",
            "batch_id",
            "created_at",
            "activated_at",
        ];
        return in_array($field, $allowed, true) ? $field : "created_at";
    }
}
