<?php

declare(strict_types=1);

namespace Instinctor\Core;

if (!defined('ABSPATH')) exit;

/**
 * MediaUsage — per-owner storage accounting.
 *
 * Maintains a running bytes_used + files_count per tenant (instinctor_owner_usage,
 * migration 0044), incremented when a tenant's file is stored and decremented
 * when it is removed. It feeds the control-panel usage meter and the soft quota.
 *
 * The vendor / single-tenant WP scope (owner null) is never metered: add() and
 * release() are no-ops there, so this is completely inert on WordPress. Counters
 * are updated with atomic SQL (no read-modify-write race), and release() floors
 * at zero so accounting drift can never go negative.
 *
 * @since 2.1.2227
 */
final class MediaUsage
{
    private static function table(): string
    {
        return DB::prefix() . 'instinctor_owner_usage';
    }

    /** Add a stored file's bytes to a tenant's tally. No-op for the vendor/WP scope. */
    public static function add(?int $ownerId, int $bytes, int $files = 1): void
    {
        if ($ownerId === null || $ownerId <= 0) {
            return;
        }
        $b = max(0, $bytes);
        $f = max(0, $files);
        if ($b === 0 && $f === 0) {
            return;
        }
        $t = self::table();
        try {
            DB::query(DB::prepare(
                "INSERT INTO `{$t}` (owner_id, bytes_used, files_count) VALUES (%d, %d, %d)
                 ON DUPLICATE KEY UPDATE bytes_used = bytes_used + %d, files_count = files_count + %d",
                $ownerId, $b, $f, $b, $f
            ));
        } catch (\Throwable) {
            // Accounting is best-effort — never fail an upload because of it.
        }
    }

    /** Subtract a removed file's bytes from a tenant's tally, floored at zero. */
    public static function release(?int $ownerId, int $bytes, int $files = 1): void
    {
        if ($ownerId === null || $ownerId <= 0) {
            return;
        }
        $b = max(0, $bytes);
        $f = max(0, $files);
        $t = self::table();
        try {
            DB::query(DB::prepare(
                "UPDATE `{$t}`
                    SET bytes_used  = GREATEST(0, CAST(bytes_used  AS SIGNED) - %d),
                        files_count = GREATEST(0, CAST(files_count AS SIGNED) - %d)
                  WHERE owner_id = %d",
                $b, $f, $ownerId
            ));
        } catch (\Throwable) {
            // best-effort
        }
    }

    /** Current usage for a tenant: ['bytes_used' => int, 'files_count' => int]. */
    public static function get(int $ownerId): array
    {
        $t = self::table();
        $row = DB::get_row(DB::prepare(
            "SELECT bytes_used, files_count FROM `{$t}` WHERE owner_id = %d",
            $ownerId
        ), 'ARRAY_A');
        return [
            'bytes_used'  => (int) ($row['bytes_used'] ?? 0),
            'files_count' => (int) ($row['files_count'] ?? 0),
        ];
    }

    /**
     * Byte size of a locally-stored file referenced by its public URL, or 0 when
     * the URL is external or the file is missing. Lets callers meter a record
     * that only holds URLs (e.g. CRM before/after photos).
     */
    public static function bytesForUrl(string $url): int
    {
        if ($url === '') {
            return 0;
        }
        $base = rtrim(Storage::baseurl(''), '/') . '/';
        if (!str_starts_with($url, $base)) {
            return 0;
        }
        $rel = ltrim(substr($url, strlen($base)), '/');
        if ($rel === '') {
            return 0;
        }
        $path = Storage::path($rel);
        return is_file($path) ? (int) (@filesize($path) ?: 0) : 0;
    }
}
