<?php

namespace KeyLair;

class ClaimLinkFactory
{
    /**
     * Resolution chain for a product's max claims on a given link, evaluated
     * once at creation time and then frozen — later changes to a campaign's,
     * creator group's, or product's default do not retroactively change
     * links already issued.
     *
     *   per-link override for this product -> creator group default ->
     *   campaign default -> product default -> 1
     *
     * $campaignDefault is nullable because a claim link's campaign is
     * itself optional (campaigns are for grouping/analytics, not required
     * to hand out keys) -- a campaign-less link just skips straight past
     * that tier to the product default.
     */
    public static function resolveMaxClaims(
        ?int $override,
        ?int $creatorGroupDefault,
        ?int $campaignDefault,
        ?int $productDefault = null
    ): int {
        if ($override !== null && $override > 0) {
            return $override;
        }
        if ($creatorGroupDefault !== null && $creatorGroupDefault > 0) {
            return $creatorGroupDefault;
        }
        if ($campaignDefault !== null && $campaignDefault > 0) {
            return $campaignDefault;
        }
        if ($productDefault !== null && $productDefault > 0) {
            return $productDefault;
        }
        return 1;
    }

    /** @param int[] $productIds @return array<int, ?int> productId => default_max_claims */
    private static function productDefaultMaxClaims(array $productIds): array
    {
        if (empty($productIds)) {
            return [];
        }
        $pdo = Database::connection();
        $placeholders = implode(',', array_fill(0, count($productIds), '?'));
        $stmt = $pdo->prepare("SELECT id, default_max_claims FROM products WHERE id IN ({$placeholders})");
        $stmt->execute(array_values($productIds));

        $defaults = [];
        foreach ($stmt->fetchAll() as $row) {
            $defaults[(int) $row['id']] = $row['default_max_claims'] !== null ? (int) $row['default_max_claims'] : null;
        }
        return $defaults;
    }

    /**
     * @param array<int, int|null> $productOverrides productId => override max_claims (or null to use the resolution chain)
     * @return array{ok: bool, token?: string, link_id?: int, error?: string}
     */
    public static function create(
        ?int $campaignId,
        int $userId,
        ?int $creatorGroupId,
        ?string $expiresAt,
        array $productOverrides
    ): array {
        if ($userId <= 0) {
            return ['ok' => false, 'error' => 'A user is required.'];
        }
        if (empty($productOverrides)) {
            return ['ok' => false, 'error' => 'Select at least one product for this link.'];
        }

        $pdo = Database::connection();

        // Campaigns are optional -- purely for grouping/analytics, never
        // required to hand out keys. A campaign default_max_claims of null
        // here just means resolveMaxClaims() skips that tier.
        $campaignDefault = null;
        if ($campaignId !== null) {
            $campaignStmt = $pdo->prepare('SELECT default_max_claims FROM campaigns WHERE id = :id');
            $campaignStmt->execute(['id' => $campaignId]);
            $campaign = $campaignStmt->fetch();
            if (!$campaign) {
                return ['ok' => false, 'error' => 'Campaign not found.'];
            }
            $campaignDefault = (int) $campaign['default_max_claims'];
        }

        $creatorGroupDefault = null;
        if ($creatorGroupId) {
            $groupStmt = $pdo->prepare('SELECT default_max_claims FROM creator_groups WHERE id = :id');
            $groupStmt->execute(['id' => $creatorGroupId]);
            $group = $groupStmt->fetch();
            if ($group) {
                $creatorGroupDefault = $group['default_max_claims'] !== null ? (int) $group['default_max_claims'] : null;
            }
        }

        $productDefaults = self::productDefaultMaxClaims(array_keys($productOverrides));

        $token = Helpers::randomToken(16);

        $pdo->beginTransaction();
        try {
            $insertLink = $pdo->prepare(
                'INSERT INTO claim_links (campaign_id, creator_group_id, user_id, token, status, expires_at, created_at)
                 VALUES (:campaign_id, :creator_group_id, :user_id, :token, "active", :expires_at, NOW())'
            );
            $insertLink->execute([
                'campaign_id' => $campaignId,
                'creator_group_id' => $creatorGroupId,
                'user_id' => $userId,
                'token' => $token,
                'expires_at' => $expiresAt ?: null,
            ]);
            $linkId = (int) $pdo->lastInsertId();

            $insertProduct = $pdo->prepare(
                'INSERT INTO claim_link_products (claim_link_id, product_id, max_claims, claims_used)
                 VALUES (:link_id, :product_id, :max_claims, 0)'
            );

            foreach ($productOverrides as $productId => $override) {
                $effective = self::resolveMaxClaims(
                    $override,
                    $creatorGroupDefault,
                    $campaignDefault,
                    $productDefaults[$productId] ?? null
                );
                $insertProduct->execute([
                    'link_id' => $linkId,
                    'product_id' => $productId,
                    'max_claims' => $effective,
                ]);
            }

            $pdo->commit();
            return ['ok' => true, 'token' => $token, 'link_id' => $linkId];
        } catch (\Throwable $e) {
            $pdo->rollBack();
            error_log('ClaimLinkFactory::create failed: ' . $e->getMessage());
            return ['ok' => false, 'error' => 'Something went wrong creating the link.'];
        }
    }

    /**
     * Updates a link's recipient details and its set of products. The
     * campaign itself is intentionally not editable here -- changing it
     * after the fact has confusing implications for reporting, and nothing
     * requested that. Products can be added or have their limit changed
     * freely; a product can only be removed if nothing has been claimed
     * against it yet on this link, and a limit can't be dropped below what's
     * already been claimed.
     *
     * @param array<int, int|null> $productOverrides productId => override max_claims (or null for the resolution chain), for every product that should remain selected
     * @return array{ok: bool, error?: string}
     */
    public static function update(
        int $linkId,
        int $userId,
        ?int $creatorGroupId,
        ?string $expiresAt,
        array $productOverrides
    ): array {
        if ($userId <= 0) {
            return ['ok' => false, 'error' => 'A user is required.'];
        }
        if (empty($productOverrides)) {
            return ['ok' => false, 'error' => 'Select at least one product for this link.'];
        }

        $pdo = Database::connection();

        $linkStmt = $pdo->prepare('SELECT * FROM claim_links WHERE id = :id');
        $linkStmt->execute(['id' => $linkId]);
        $link = $linkStmt->fetch();
        if (!$link) {
            return ['ok' => false, 'error' => 'Claim link not found.'];
        }

        $campaignDefault = null;
        if ($link['campaign_id'] !== null) {
            $campaignStmt = $pdo->prepare('SELECT default_max_claims FROM campaigns WHERE id = :id');
            $campaignStmt->execute(['id' => $link['campaign_id']]);
            $campaign = $campaignStmt->fetch();
            if ($campaign) {
                $campaignDefault = (int) $campaign['default_max_claims'];
            }
        }

        $creatorGroupDefault = null;
        if ($creatorGroupId) {
            $groupStmt = $pdo->prepare('SELECT default_max_claims FROM creator_groups WHERE id = :id');
            $groupStmt->execute(['id' => $creatorGroupId]);
            $group = $groupStmt->fetch();
            if ($group) {
                $creatorGroupDefault = $group['default_max_claims'] !== null ? (int) $group['default_max_claims'] : null;
            }
        }

        $existingStmt = $pdo->prepare('SELECT product_id, max_claims, claims_used FROM claim_link_products WHERE claim_link_id = :id');
        $existingStmt->execute(['id' => $linkId]);
        $existing = [];
        foreach ($existingStmt->fetchAll() as $row) {
            $existing[(int) $row['product_id']] = $row;
        }

        // Validate before writing anything: a product being removed or
        // shrunk below its already-claimed amount is a user error, not
        // something to silently clamp.
        foreach ($existing as $productId => $row) {
            if (!array_key_exists($productId, $productOverrides) && (int) $row['claims_used'] > 0) {
                return ['ok' => false, 'error' => "Can't remove a product that already has claims against it on this link."];
            }
        }
        foreach ($productOverrides as $productId => $override) {
            if (isset($existing[$productId]) && $override !== null && $override < (int) $existing[$productId]['claims_used']) {
                return ['ok' => false, 'error' => "Can't set a limit below what's already been claimed ({$existing[$productId]['claims_used']})."];
            }
        }

        $pdo->beginTransaction();
        try {
            $pdo->prepare(
                'UPDATE claim_links SET user_id = :user_id, creator_group_id = :creator_group_id, expires_at = :expires_at WHERE id = :id'
            )->execute([
                'user_id' => $userId,
                'creator_group_id' => $creatorGroupId,
                'expires_at' => $expiresAt ?: null,
                'id' => $linkId,
            ]);

            foreach ($existing as $productId => $row) {
                if (!array_key_exists($productId, $productOverrides)) {
                    $pdo->prepare('DELETE FROM claim_link_products WHERE claim_link_id = :link_id AND product_id = :product_id')
                        ->execute(['link_id' => $linkId, 'product_id' => $productId]);
                }
            }

            $productDefaults = self::productDefaultMaxClaims(array_keys($productOverrides));

            $upsert = $pdo->prepare(
                'INSERT INTO claim_link_products (claim_link_id, product_id, max_claims, claims_used)
                 VALUES (:link_id, :product_id, :max_claims, 0)
                 ON DUPLICATE KEY UPDATE max_claims = :max_claims2'
            );
            foreach ($productOverrides as $productId => $override) {
                $effective = $override ?? self::resolveMaxClaims(null, $creatorGroupDefault, $campaignDefault, $productDefaults[$productId] ?? null);
                $upsert->execute([
                    'link_id' => $linkId,
                    'product_id' => $productId,
                    'max_claims' => $effective,
                    'max_claims2' => $effective,
                ]);
            }

            $pdo->commit();
            return ['ok' => true];
        } catch (\Throwable $e) {
            $pdo->rollBack();
            error_log('ClaimLinkFactory::update failed: ' . $e->getMessage());
            return ['ok' => false, 'error' => 'Something went wrong updating the link.'];
        }
    }
}
