<?php
namespace EncypherProvenance;

/**
 * Signing lease helpers for `_encypher_signing_in_progress` post meta.
 *
 * New values are tokenized leases with `started_at`, `heartbeat_at`, and
 * `owner`. Legacy timestamp values remain readable so existing queued work can
 * self-heal after an upgrade.
 *
 * Value helpers remain dependency-free for standalone tests. The acquire,
 * heartbeat, and release methods are the single WordPress post-meta boundary.
 */
class SigningStatus
{
    public const STALE_AFTER_SECONDS = 300;

    /**
     * Create a new tokenized lease.
     *
     * @return array{token:string,started_at:int,heartbeat_at:int,owner:string}
     */
    public static function new_lease(string $owner, int $now, ?string $token = null): array
    {
        if (null === $token || '' === $token) {
            $token = function_exists('wp_generate_uuid4')
                ? wp_generate_uuid4()
                : bin2hex(random_bytes(16));
        }

        return [
            'token'        => $token,
            'started_at'   => $now,
            'heartbeat_at' => $now,
            'owner'        => $owner,
        ];
    }

    /**
     * Return a renewed lease only when the caller still owns it.
     *
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     * @return array{token:string,started_at:int,heartbeat_at:int,owner:string}|null
     */
    public static function renew($raw_meta_value, string $token, int $now): ?array
    {
        if (! self::token_matches($raw_meta_value, $token)) {
            return null;
        }

        $lease = $raw_meta_value;
        $lease['heartbeat_at'] = $now;
        return $lease;
    }

    /**
     * Whether a raw lease value is stale.
     *
     * Empty values mean no work is in progress. Legacy numeric timestamps use
     * their timestamp as the heartbeat. Malformed values fail closed as stale.
     *
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     */
    public static function flag_is_stale($raw_meta_value, int $now): bool
    {
        if (empty($raw_meta_value)) {
            return false;
        }

        $heartbeat = self::heartbeat_at($raw_meta_value);
        if (null === $heartbeat) {
            return true;
        }

        return ($now - $heartbeat) > self::STALE_AFTER_SECONDS;
    }

    /**
     * Return the lease start time for stall/error ordering.
     *
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     */
    public static function started_at($raw_meta_value): int
    {
        if (is_array($raw_meta_value)) {
            return max(0, (int) ($raw_meta_value['started_at'] ?? 0));
        }
        return is_numeric($raw_meta_value) ? max(0, (int) $raw_meta_value) : 0;
    }

    /**
     * Return the owner token for a valid tokenized lease.
     *
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     */
    public static function token($raw_meta_value): string
    {
        if (! is_array($raw_meta_value)) {
            return '';
        }
        $token = $raw_meta_value['token'] ?? '';
        return is_string($token) ? $token : '';
    }

    /**
     * Return the current lease owner label.
     *
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     */
    public static function owner($raw_meta_value): string
    {
        if (! is_array($raw_meta_value)) {
            return '';
        }
        $owner = $raw_meta_value['owner'] ?? '';
        return is_string($owner) ? $owner : '';
    }

    /**
     * Whether the raw lease belongs to the supplied token.
     *
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     */
    public static function token_matches($raw_meta_value, string $token): bool
    {
        $stored = self::token($raw_meta_value);
        return '' !== $stored && '' !== $token && hash_equals($stored, $token);
    }

    /**
     * Acquire the current fresh lease or replace an absent/stale legacy value.
     */
    public static function acquire(int $post_id, string $owner, ?int $now = null): string
    {
        $now = $now ?? time();
        $raw = get_post_meta($post_id, '_encypher_signing_in_progress', true);
        $token = self::token($raw);
        if ('' !== $token && ! self::flag_is_stale($raw, $now)) {
            $renewed = self::renew($raw, $token, $now);
            if (null !== $renewed) {
                update_post_meta($post_id, '_encypher_signing_in_progress', $renewed);
                return $token;
            }
        }

        $lease = self::new_lease($owner, $now);
        update_post_meta($post_id, '_encypher_signing_in_progress', $lease);
        return $lease['token'];
    }

    /**
     * Transition a queued lease to its worker while preserving the token.
     */
    public static function claim(int $post_id, string $owner, ?int $now = null): string
    {
        $now = $now ?? time();
        $raw = get_post_meta($post_id, '_encypher_signing_in_progress', true);
        $token = self::token($raw);
        if ('' === $token || self::flag_is_stale($raw, $now)) {
            return self::acquire($post_id, $owner, $now);
        }

        $renewed = self::renew($raw, $token, $now);
        if (null !== $renewed) {
            $renewed['owner'] = $owner;
            update_post_meta($post_id, '_encypher_signing_in_progress', $renewed, $raw);
        }
        return $token;
    }

    /**
     * Renew a lease only while the caller still owns the stored token.
     */
    public static function heartbeat(int $post_id, string $token, ?int $now = null): bool
    {
        $raw = get_post_meta($post_id, '_encypher_signing_in_progress', true);
        $renewed = self::renew($raw, $token, $now ?? time());
        if (null === $renewed) {
            return false;
        }

        return $renewed === $raw || false !== update_post_meta($post_id, '_encypher_signing_in_progress', $renewed, $raw);
    }

    /**
     * Release a lease only while the caller still owns the stored token.
     */
    public static function release(int $post_id, string $token): bool
    {
        $raw = get_post_meta($post_id, '_encypher_signing_in_progress', true);
        if (! self::token_matches($raw, $token)) {
            return false;
        }

        return false !== delete_post_meta($post_id, '_encypher_signing_in_progress', $raw);
    }

    /**
     * Remove the exact stale value observed by a status reader.
     *
     * A concurrent heartbeat changes the serialized value, so WordPress leaves
     * the renewed lease intact.
     *
     * @param mixed $raw_meta_value Raw value already read by the caller.
     */
    public static function clear_if_stale(int $post_id, $raw_meta_value, int $now): bool
    {
        if (! self::flag_is_stale($raw_meta_value, $now)) {
            return false;
        }

        return false !== delete_post_meta($post_id, '_encypher_signing_in_progress', $raw_meta_value);
    }


    /**
     * @param mixed $raw_meta_value Raw value as returned by get_post_meta().
     */
    private static function heartbeat_at($raw_meta_value): ?int
    {
        if (is_numeric($raw_meta_value)) {
            return (int) $raw_meta_value;
        }
        if (! is_array($raw_meta_value)) {
            return null;
        }

        $heartbeat = $raw_meta_value['heartbeat_at'] ?? null;
        $started = $raw_meta_value['started_at'] ?? null;
        if (! is_numeric($heartbeat) || ! is_numeric($started) || '' === self::token($raw_meta_value)) {
            return null;
        }

        return (int) $heartbeat;
    }
}
