<?php
/**
 * License Manager
 *
 * @package Jeremias\WcLicensedProduct\License
 */

declare(strict_types=1);

namespace Jeremias\WcLicensedProduct\License;

use Jeremias\WcLicensedProduct\Installer;
use Jeremias\WcLicensedProduct\Product\LicensedProduct;
use Jeremias\WcLicensedProduct\Product\LicensedProductVariation;
use Jeremias\WcLicensedProduct\Product\LicensedVariableProduct;

/**
 * Manages license operations (CRUD, validation, generation)
 */
class LicenseManager
{
    /**
     * Check if a product is any type of licensed product
     *
     * @param \WC_Product $product Product to check
     * @return bool True if product is licensed (simple or variable or variation)
     */
    public function isLicensedProduct(\WC_Product $product): bool
    {
        // Simple licensed product
        if ($product->is_type('licensed')) {
            return true;
        }

        // Variable licensed product
        if ($product->is_type('licensed-variable')) {
            return true;
        }

        // Variation of a licensed-variable product
        if ($product->is_type('variation') && $product->get_parent_id()) {
            $parent = wc_get_product($product->get_parent_id());
            if ($parent && $parent->is_type('licensed-variable')) {
                return true;
            }
        }

        return false;
    }

    /**
     * Generate a unique license key
     */
    public function generateLicenseKey(): string
    {
        // Format: XXXX-XXXX-XXXX-XXXX (32 chars hex, 4 groups)
        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $key = '';

        for ($i = 0; $i < 4; $i++) {
            if ($i > 0) {
                $key .= '-';
            }
            for ($j = 0; $j < 4; $j++) {
                $key .= $chars[random_int(0, strlen($chars) - 1)];
            }
        }

        return $key;
    }

    /**
     * Generate a license for a completed order
     *
     * @param int $orderId Order ID
     * @param int $productId Product ID (parent product for variations)
     * @param int $customerId Customer ID
     * @param string $domain Domain to bind the license to
     * @param int|null $variationId Optional variation ID for variable licensed products
     * @return License|null Generated license or null on failure
     */
    public function generateLicense(
        int $orderId,
        int $productId,
        int $customerId,
        string $domain,
        ?int $variationId = null
    ): ?License {
        global $wpdb;

        // Normalize domain first for duplicate detection
        $normalizedDomain = $this->normalizeDomain($domain);

        // Check if license already exists for this order, product, domain, and variation
        $existing = $this->getLicenseByOrderProductDomainAndVariation($orderId, $productId, $normalizedDomain, $variationId);
        if ($existing) {
            return $existing;
        }

        // Load the product that has the license settings
        // For variations, load the variation; otherwise load the parent product
        if ($variationId) {
            $settingsProduct = wc_get_product($variationId);
            $parentProduct = wc_get_product($productId);

            // Verify parent is licensed-variable
            if (!$parentProduct || !$parentProduct->is_type('licensed-variable')) {
                return null;
            }

            // Ensure we have the proper variation class
            if ($settingsProduct && !$settingsProduct instanceof LicensedProductVariation) {
                $settingsProduct = new LicensedProductVariation($variationId);
            }
        } else {
            $settingsProduct = wc_get_product($productId);

            // Check if this is a licensed product (simple)
            if (!$settingsProduct || !$settingsProduct->is_type('licensed')) {
                return null;
            }

            // Ensure we have the LicensedProduct instance for type hints
            if (!$settingsProduct instanceof LicensedProduct) {
                $settingsProduct = new LicensedProduct($productId);
            }
        }

        if (!$settingsProduct) {
            return null;
        }

        // Generate unique license key
        $licenseKey = $this->generateLicenseKey();
        while ($this->getLicenseByKey($licenseKey)) {
            $licenseKey = $this->generateLicenseKey();
        }

        // Calculate expiration date from the settings product (variation or parent)
        $expiresAt = null;
        $validityDays = $settingsProduct->get_validity_days();
        if ($validityDays !== null && $validityDays > 0) {
            $expiresAt = (new \DateTimeImmutable())->modify("+{$validityDays} days")->format('Y-m-d H:i:s');
        }

        // Determine version ID if bound to version (always use parent product ID for versions)
        $versionId = null;
        if ($settingsProduct->is_bound_to_version()) {
            $versionId = $this->getCurrentVersionId($productId);
        }

        $tableName = Installer::getLicensesTable();
        $result = $wpdb->insert(
            $tableName,
            [
                'license_key' => $licenseKey,
                'order_id' => $orderId,
                'product_id' => $productId,
                'customer_id' => $customerId,
                'domain' => $this->normalizeDomain($domain),
                'version_id' => $versionId,
                'status' => License::STATUS_ACTIVE,
                'activations_count' => 1,
                'max_activations' => $settingsProduct->get_max_activations(),
                'expires_at' => $expiresAt,
            ],
            ['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
        );

        if ($result === false) {
            return null;
        }

        return $this->getLicenseById((int) $wpdb->insert_id);
    }

    /**
     * Get license by order, product, domain, and optional variation
     */
    public function getLicenseByOrderProductDomainAndVariation(int $orderId, int $productId, string $domain, ?int $variationId = null): ?License
    {
        // For now, just use the existing method since we don't store variation_id in licenses table yet
        // In the future, we could add a variation_id column to the licenses table
        return $this->getLicenseByOrderProductAndDomain($orderId, $productId, $domain);
    }

    /**
     * Get license by ID
     */
    public function getLicenseById(int $id): ?License
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$tableName} WHERE id = %d", $id),
            ARRAY_A
        );

        return $row ? License::fromArray($row) : null;
    }

    /**
     * Get license by license key
     */
    public function getLicenseByKey(string $licenseKey): ?License
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $row = $wpdb->get_row(
            $wpdb->prepare("SELECT * FROM {$tableName} WHERE license_key = %s", $licenseKey),
            ARRAY_A
        );

        return $row ? License::fromArray($row) : null;
    }

    /**
     * Get license by order and product
     */
    public function getLicenseByOrderAndProduct(int $orderId, int $productId): ?License
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d",
                $orderId,
                $productId
            ),
            ARRAY_A
        );

        return $row ? License::fromArray($row) : null;
    }

    /**
     * Get all licenses for an order and product
     *
     * @return License[]
     */
    public function getLicensesByOrderAndProduct(int $orderId, int $productId): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d ORDER BY created_at ASC",
                $orderId,
                $productId
            ),
            ARRAY_A
        );

        return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
    }

    /**
     * Get license by order, product, and domain
     */
    public function getLicenseByOrderProductAndDomain(int $orderId, int $productId, string $domain): ?License
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $row = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$tableName} WHERE order_id = %d AND product_id = %d AND domain = %s",
                $orderId,
                $productId,
                $domain
            ),
            ARRAY_A
        );

        return $row ? License::fromArray($row) : null;
    }

    /**
     * Get all licenses for an order
     */
    public function getLicensesByOrder(int $orderId): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$tableName} WHERE order_id = %d ORDER BY created_at DESC",
                $orderId
            ),
            ARRAY_A
        );

        return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
    }

    /**
     * Get all licenses for a customer
     */
    public function getLicensesByCustomer(int $customerId): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$tableName} WHERE customer_id = %d ORDER BY created_at DESC",
                $customerId
            ),
            ARRAY_A
        );

        return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
    }

    /**
     * Get all licenses (for admin) with optional filtering
     *
     * @param int $page Page number
     * @param int $perPage Items per page
     * @param array $filters Optional filters: search, status, product_id, customer_id
     * @return array Array of License objects
     */
    public function getAllLicenses(int $page = 1, int $perPage = 20, array $filters = []): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $offset = ($page - 1) * $perPage;

        $where = [];
        $params = [];

        // Search filter (searches license key, domain, customer email)
        if (!empty($filters['search'])) {
            $search = '%' . $wpdb->esc_like($filters['search']) . '%';
            $where[] = "(license_key LIKE %s OR domain LIKE %s)";
            $params[] = $search;
            $params[] = $search;
        }

        // Status filter
        if (!empty($filters['status']) && in_array($filters['status'], [
            License::STATUS_ACTIVE,
            License::STATUS_INACTIVE,
            License::STATUS_EXPIRED,
            License::STATUS_REVOKED,
        ], true)) {
            $where[] = "status = %s";
            $params[] = $filters['status'];
        }

        // Product filter
        if (!empty($filters['product_id'])) {
            $where[] = "product_id = %d";
            $params[] = absint($filters['product_id']);
        }

        // Customer filter
        if (!empty($filters['customer_id'])) {
            $where[] = "customer_id = %d";
            $params[] = absint($filters['customer_id']);
        }

        $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';

        $params[] = $perPage;
        $params[] = $offset;

        $sql = "SELECT * FROM {$tableName} {$whereClause} ORDER BY created_at DESC LIMIT %d OFFSET %d";

        $rows = $wpdb->get_results(
            $wpdb->prepare($sql, $params),
            ARRAY_A
        );

        return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
    }

    /**
     * Get total license count with optional filtering
     *
     * @param array $filters Optional filters: search, status, product_id, customer_id
     * @return int Total count
     */
    public function getLicenseCount(array $filters = []): int
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();

        $where = [];
        $params = [];

        // Search filter
        if (!empty($filters['search'])) {
            $search = '%' . $wpdb->esc_like($filters['search']) . '%';
            $where[] = "(license_key LIKE %s OR domain LIKE %s)";
            $params[] = $search;
            $params[] = $search;
        }

        // Status filter
        if (!empty($filters['status']) && in_array($filters['status'], [
            License::STATUS_ACTIVE,
            License::STATUS_INACTIVE,
            License::STATUS_EXPIRED,
            License::STATUS_REVOKED,
        ], true)) {
            $where[] = "status = %s";
            $params[] = $filters['status'];
        }

        // Product filter
        if (!empty($filters['product_id'])) {
            $where[] = "product_id = %d";
            $params[] = absint($filters['product_id']);
        }

        // Customer filter
        if (!empty($filters['customer_id'])) {
            $where[] = "customer_id = %d";
            $params[] = absint($filters['customer_id']);
        }

        $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';

        if (empty($params)) {
            return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
        }

        return (int) $wpdb->get_var(
            $wpdb->prepare("SELECT COUNT(*) FROM {$tableName} {$whereClause}", $params)
        );
    }

    /**
     * Get all licensed products for filter dropdown
     *
     * @return array Array of [id => name] pairs
     */
    public function getLicensedProducts(): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $productIds = $wpdb->get_col("SELECT DISTINCT product_id FROM {$tableName}");

        $products = [];
        foreach ($productIds as $productId) {
            $product = wc_get_product((int) $productId);
            if ($product) {
                $products[(int) $productId] = $product->get_name();
            }
        }

        return $products;
    }

    /**
     * Validate a license key for a domain
     */
    public function validateLicense(string $licenseKey, string $domain): array
    {
        $license = $this->getLicenseByKey($licenseKey);

        if (!$license) {
            return [
                'valid' => false,
                'error' => 'license_not_found',
                'message' => __('License key not found.', 'wc-licensed-product'),
            ];
        }

        // Check license status
        if ($license->getStatus() === License::STATUS_REVOKED) {
            return [
                'valid' => false,
                'error' => 'license_revoked',
                'message' => __('This license has been revoked.', 'wc-licensed-product'),
            ];
        }

        // Check expiration
        if ($license->isExpired()) {
            $this->updateLicenseStatus($license->getId(), License::STATUS_EXPIRED);
            return [
                'valid' => false,
                'error' => 'license_expired',
                'message' => __('This license has expired.', 'wc-licensed-product'),
            ];
        }

        if ($license->getStatus() === License::STATUS_INACTIVE) {
            return [
                'valid' => false,
                'error' => 'license_inactive',
                'message' => __('This license is inactive.', 'wc-licensed-product'),
            ];
        }

        // Check domain
        $normalizedDomain = $this->normalizeDomain($domain);
        if ($license->getDomain() !== $normalizedDomain) {
            return [
                'valid' => false,
                'error' => 'domain_mismatch',
                'message' => __('This license is not valid for this domain.', 'wc-licensed-product'),
            ];
        }

        return [
            'valid' => true,
            'license' => [
                'product_id' => $license->getProductId(),
                'expires_at' => $license->getExpiresAt()?->format('Y-m-d'),
                'version_id' => $license->getVersionId(),
            ],
        ];
    }

    /**
     * Update license status
     */
    public function updateLicenseStatus(int $licenseId, string $status): bool
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $result = $wpdb->update(
            $tableName,
            ['status' => $status],
            ['id' => $licenseId],
            ['%s'],
            ['%d']
        );

        return $result !== false;
    }

    /**
     * Update license expiry date
     *
     * @param int $licenseId License ID
     * @param \DateTimeImmutable $expiresAt New expiry date
     * @return bool Success
     */
    public function updateLicenseExpiry(int $licenseId, \DateTimeImmutable $expiresAt): bool
    {
        global $wpdb;

        $license = $this->getLicenseById($licenseId);
        if (!$license) {
            return false;
        }

        $tableName = Installer::getLicensesTable();
        $result = $wpdb->update(
            $tableName,
            ['expires_at' => $expiresAt->format('Y-m-d H:i:s')],
            ['id' => $licenseId],
            ['%s'],
            ['%d']
        );

        // If license was expired and new date is in the future, reactivate it
        if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED && $expiresAt > new \DateTimeImmutable()) {
            $this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
        }

        return $result !== false;
    }

    /**
     * Update license domain
     */
    public function updateLicenseDomain(int $licenseId, string $domain): bool
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $result = $wpdb->update(
            $tableName,
            ['domain' => $this->normalizeDomain($domain)],
            ['id' => $licenseId],
            ['%s'],
            ['%d']
        );

        return $result !== false;
    }

    /**
     * Delete a license
     */
    public function deleteLicense(int $licenseId): bool
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $result = $wpdb->delete(
            $tableName,
            ['id' => $licenseId],
            ['%d']
        );

        return $result !== false;
    }

    /**
     * Normalize domain name
     */
    public function normalizeDomain(string $domain): string
    {
        // Remove protocol
        $domain = preg_replace('#^https?://#', '', $domain);

        // Remove trailing slash and path
        $domain = preg_replace('#/.*$#', '', $domain);

        // Remove www prefix
        $domain = preg_replace('#^www\.#', '', $domain);

        // Lowercase
        return strtolower(trim($domain));
    }

    /**
     * Get current version ID for a product
     */
    private function getCurrentVersionId(int $productId): ?int
    {
        global $wpdb;

        $tableName = Installer::getVersionsTable();
        $versionId = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT id FROM {$tableName} WHERE product_id = %d AND is_active = 1 ORDER BY released_at DESC LIMIT 1",
                $productId
            )
        );

        return $versionId ? (int) $versionId : null;
    }

    /**
     * Extend license expiration
     *
     * @param int $licenseId License ID
     * @param int $days Number of days to extend
     * @return bool Success
     */
    public function extendLicense(int $licenseId, int $days): bool
    {
        global $wpdb;

        $license = $this->getLicenseById($licenseId);
        if (!$license) {
            return false;
        }

        // Calculate new expiration date
        $currentExpiry = $license->getExpiresAt();
        if ($currentExpiry === null) {
            // License is lifetime, set expiration from now
            $newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
        } elseif ($currentExpiry < new \DateTimeImmutable()) {
            // License is expired, extend from now
            $newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
        } else {
            // License still valid, extend from current expiry
            $newExpiry = \DateTimeImmutable::createFromInterface($currentExpiry)->modify("+{$days} days");
        }

        $tableName = Installer::getLicensesTable();
        $result = $wpdb->update(
            $tableName,
            ['expires_at' => $newExpiry->format('Y-m-d H:i:s')],
            ['id' => $licenseId],
            ['%s'],
            ['%d']
        );

        // If license was expired, reactivate it
        if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED) {
            $this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
        }

        return $result !== false;
    }

    /**
     * Set license to lifetime (no expiration)
     *
     * @param int $licenseId License ID
     * @return bool Success
     */
    public function setLicenseLifetime(int $licenseId): bool
    {
        global $wpdb;

        $license = $this->getLicenseById($licenseId);

        $tableName = Installer::getLicensesTable();
        // Use raw query to set NULL
        $result = $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$tableName} SET expires_at = NULL WHERE id = %d",
                $licenseId
            )
        );

        // If license was expired, reactivate it
        if ($result !== false && $license && $license->getStatus() === License::STATUS_EXPIRED) {
            $this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
        }

        return $result !== false;
    }

    /**
     * Bulk update license status
     *
     * @param array $licenseIds Array of license IDs
     * @param string $status New status
     * @return int Number of licenses updated
     */
    public function bulkUpdateStatus(array $licenseIds, string $status): int
    {
        global $wpdb;

        if (empty($licenseIds)) {
            return 0;
        }

        $tableName = Installer::getLicensesTable();
        $ids = array_map('absint', $licenseIds);
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));

        $result = $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$tableName} SET status = %s WHERE id IN ({$placeholders})",
                array_merge([$status], $ids)
            )
        );

        return $result !== false ? (int) $result : 0;
    }

    /**
     * Bulk delete licenses
     *
     * @param array $licenseIds Array of license IDs
     * @return int Number of licenses deleted
     */
    public function bulkDelete(array $licenseIds): int
    {
        global $wpdb;

        if (empty($licenseIds)) {
            return 0;
        }

        $tableName = Installer::getLicensesTable();
        $ids = array_map('absint', $licenseIds);
        $placeholders = implode(',', array_fill(0, count($ids), '%d'));

        $result = $wpdb->query(
            $wpdb->prepare(
                "DELETE FROM {$tableName} WHERE id IN ({$placeholders})",
                $ids
            )
        );

        return $result !== false ? (int) $result : 0;
    }

    /**
     * Bulk extend licenses
     *
     * @param array $licenseIds Array of license IDs
     * @param int $days Number of days to extend
     * @return int Number of licenses extended
     */
    public function bulkExtend(array $licenseIds, int $days): int
    {
        $count = 0;
        foreach ($licenseIds as $licenseId) {
            if ($this->extendLicense((int) $licenseId, $days)) {
                $count++;
            }
        }
        return $count;
    }

    /**
     * Transfer license to a new domain
     *
     * @param int $licenseId License ID
     * @param string $newDomain New domain to transfer to
     * @return bool Success
     */
    public function transferLicense(int $licenseId, string $newDomain): bool
    {
        $license = $this->getLicenseById($licenseId);
        if (!$license) {
            return false;
        }

        // Cannot transfer revoked licenses
        if ($license->getStatus() === License::STATUS_REVOKED) {
            return false;
        }

        return $this->updateLicenseDomain($licenseId, $newDomain);
    }

    /**
     * Get license statistics
     *
     * @return array Statistics data
     */
    public function getStatistics(): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();

        // Get counts by status
        $statusCounts = $wpdb->get_results(
            "SELECT status, COUNT(*) as count FROM {$tableName} GROUP BY status",
            ARRAY_A
        );

        $byStatus = [
            License::STATUS_ACTIVE => 0,
            License::STATUS_INACTIVE => 0,
            License::STATUS_EXPIRED => 0,
            License::STATUS_REVOKED => 0,
        ];
        foreach ($statusCounts ?: [] as $row) {
            $byStatus[$row['status']] = (int) $row['count'];
        }

        // Get total count
        $total = array_sum($byStatus);

        // Get lifetime vs expiring licenses
        $lifetimeCount = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NULL"
        );
        $expiringCount = $total - $lifetimeCount;

        // Get licenses expiring soon (next 30 days)
        $expiringSoon = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NOT NULL AND expires_at <= %s AND expires_at > NOW() AND status = %s",
                (new \DateTimeImmutable())->modify('+30 days')->format('Y-m-d H:i:s'),
                License::STATUS_ACTIVE
            )
        );

        // Get licenses by product
        $byProduct = $wpdb->get_results(
            "SELECT product_id, COUNT(*) as count FROM {$tableName} GROUP BY product_id ORDER BY count DESC LIMIT 10",
            ARRAY_A
        );

        $productStats = [];
        foreach ($byProduct ?: [] as $row) {
            $product = wc_get_product((int) $row['product_id']);
            $productStats[] = [
                'product_id' => (int) $row['product_id'],
                'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
                'count' => (int) $row['count'],
            ];
        }

        // Get licenses created per month (last 12 months)
        $monthlyData = $wpdb->get_results(
            "SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count
             FROM {$tableName}
             WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
             GROUP BY DATE_FORMAT(created_at, '%Y-%m')
             ORDER BY month ASC",
            ARRAY_A
        );

        $monthlyStats = [];
        foreach ($monthlyData ?: [] as $row) {
            $monthlyStats[$row['month']] = (int) $row['count'];
        }

        // Get top domains
        $topDomains = $wpdb->get_results(
            "SELECT domain, COUNT(*) as count FROM {$tableName} GROUP BY domain ORDER BY count DESC LIMIT 10",
            ARRAY_A
        );

        return [
            'total' => $total,
            'by_status' => $byStatus,
            'lifetime' => $lifetimeCount,
            'expiring' => $expiringCount,
            'expiring_soon' => $expiringSoon,
            'by_product' => $productStats,
            'monthly' => $monthlyStats,
            'top_domains' => $topDomains ?: [],
        ];
    }

    /**
     * Get licenses expiring within specified days
     *
     * @param int $days Number of days to look ahead
     * @param bool $excludeNotified Whether to exclude already notified licenses
     * @return array Array of License objects with customer data
     */
    public function getLicensesExpiringSoon(int $days = 7, bool $excludeNotified = true): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $now = new \DateTimeImmutable();
        $future = $now->modify("+{$days} days");

        $sql = "SELECT * FROM {$tableName}
                WHERE expires_at IS NOT NULL
                AND expires_at > %s
                AND expires_at <= %s
                AND status = %s";

        $params = [
            $now->format('Y-m-d H:i:s'),
            $future->format('Y-m-d H:i:s'),
            License::STATUS_ACTIVE,
        ];

        $rows = $wpdb->get_results(
            $wpdb->prepare($sql, $params),
            ARRAY_A
        );

        return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
    }

    /**
     * Mark license as notified for expiration warning
     *
     * @param int $licenseId License ID
     * @param string $notificationType Type of notification (e.g., 'expiring_7_days', 'expiring_1_day')
     * @return bool Success
     */
    public function markExpirationNotified(int $licenseId, string $notificationType): bool
    {
        $metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType);
        update_user_meta($this->getLicenseById($licenseId)?->getCustomerId() ?? 0, $metaKey . '_' . $licenseId, current_time('mysql'));
        return true;
    }

    /**
     * Check if license was already notified for expiration
     *
     * @param int $licenseId License ID
     * @param string $notificationType Type of notification
     * @return bool Whether already notified
     */
    public function wasExpirationNotified(int $licenseId, string $notificationType): bool
    {
        $license = $this->getLicenseById($licenseId);
        if (!$license) {
            return true; // Consider notified if license doesn't exist
        }

        $metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType) . '_' . $licenseId;
        return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
    }

    /**
     * Get licenses that have passed their expiration date but are still marked as active
     *
     * @return array Array of License objects that need to be auto-expired
     */
    public function getExpiredActiveLicenses(): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $now = new \DateTimeImmutable();

        $sql = "SELECT * FROM {$tableName}
                WHERE expires_at IS NOT NULL
                AND expires_at < %s
                AND status = %s";

        $rows = $wpdb->get_results(
            $wpdb->prepare($sql, $now->format('Y-m-d H:i:s'), License::STATUS_ACTIVE),
            ARRAY_A
        );

        return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
    }

    /**
     * Auto-expire a license and return true if status was changed
     *
     * @param int $licenseId License ID
     * @return bool True if license was expired, false if already expired or error
     */
    public function autoExpireLicense(int $licenseId): bool
    {
        $license = $this->getLicenseById($licenseId);
        if (!$license) {
            return false;
        }

        // Only expire if currently active and past expiration date
        if ($license->getStatus() !== License::STATUS_ACTIVE) {
            return false;
        }

        if (!$license->isExpired()) {
            return false;
        }

        return $this->updateLicenseStatus($licenseId, License::STATUS_EXPIRED);
    }

    /**
     * Import a license from CSV data
     *
     * @param string $licenseKey License key
     * @param int $productId Product ID
     * @param int $customerId Customer ID
     * @param string $domain Domain name
     * @param int $orderId Order ID (optional)
     * @param string $status License status
     * @param int $maxActivations Maximum activations
     * @param int $activationsCount Current activation count
     * @param \DateTimeImmutable|null $expiresAt Expiration date or null for lifetime
     * @return bool Success
     */
    public function importLicense(
        string $licenseKey,
        int $productId,
        int $customerId,
        string $domain,
        int $orderId = 0,
        string $status = License::STATUS_ACTIVE,
        int $maxActivations = 1,
        int $activationsCount = 1,
        ?\DateTimeImmutable $expiresAt = null
    ): bool {
        global $wpdb;

        $tableName = Installer::getLicensesTable();

        $result = $wpdb->insert(
            $tableName,
            [
                'license_key' => $licenseKey,
                'order_id' => $orderId,
                'product_id' => $productId,
                'customer_id' => $customerId,
                'domain' => $this->normalizeDomain($domain),
                'version_id' => null,
                'status' => $status,
                'activations_count' => $activationsCount,
                'max_activations' => $maxActivations,
                'expires_at' => $expiresAt ? $expiresAt->format('Y-m-d H:i:s') : null,
            ],
            ['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
        );

        return $result !== false;
    }

    /**
     * Export all licenses to array format suitable for CSV
     *
     * @return array Array of license data for CSV export
     */
    public function exportLicensesForCsv(): array
    {
        global $wpdb;

        $tableName = Installer::getLicensesTable();
        $rows = $wpdb->get_results(
            "SELECT * FROM {$tableName} ORDER BY created_at DESC",
            ARRAY_A
        );

        $exportData = [];
        foreach ($rows ?: [] as $row) {
            $product = wc_get_product((int) $row['product_id']);
            $customer = get_userdata((int) $row['customer_id']);
            $order = wc_get_order((int) $row['order_id']);

            $exportData[] = [
                'ID' => $row['id'],
                'License Key' => $row['license_key'],
                'Product' => $product ? $product->get_name() : 'Unknown',
                'Product ID' => $row['product_id'],
                'Order ID' => $row['order_id'],
                'Order Number' => $order ? $order->get_order_number() : '',
                'Customer' => $customer ? $customer->display_name : 'Guest',
                'Customer Email' => $customer ? $customer->user_email : '',
                'Customer ID' => $row['customer_id'],
                'Domain' => $row['domain'],
                'Status' => ucfirst($row['status']),
                'Activations' => $row['activations_count'],
                'Max Activations' => $row['max_activations'],
                'Expires At' => $row['expires_at'] ?: 'Lifetime',
                'Created At' => $row['created_at'],
                'Updated At' => $row['updated_at'],
            ];
        }

        return $exportData;
    }
}
