<?php

declare(strict_types=1);

namespace Instinctor\Services;

use Instinctor\Core\DB;
use Instinctor\Core\Tenant;

/**
 * The gate every service-photo write goes through.
 *
 * Owners upload their own work photos, and a trade with a phone full of job pictures
 * will happily attach ninety of them to one service. That is their disk bill and our
 * page weight, so the ceiling is enforced HERE rather than in whatever UI happens to
 * call it: a cap that lives in a form is a cap that the next form forgets
 * (Sergei 2026-07-28).
 *
 * The render side (impulse_marquee_service_photos) caps again on the way out. That is
 * deliberate belt-and-braces: rows written before this class existed, or by a script,
 * must not be able to put a hundred images on a page.
 *
 * @since 2.1.2677
 */
final class ServicePhotos
{
    /** Photos per service. Twelve fills a marquee row without repeating too obviously. */
    public const MAX_PER_SERVICE = 12;

    /** Services that may carry a strip on Pro. Beyond four it stops being a portfolio. */
    public const MAX_SERVICES = 4;

    /**
     * Free gets ONE complete strip.
     *
     * Not a crippled row: the marquee repeats whatever it is given until a set clears
     * 4000px, so one service with two photos cycles exactly as smoothly as one with
     * twelve. A free trade therefore sees the feature working properly on one service
     * and can see, locked, that the others are available (Sergei 2026-07-28).
     *
     * The limit is storage, and that is a real cost we can state plainly rather than an
     * artificial throttle: photographs of finished work are the one thing a trade has a
     * lot of, and they live on our disk.
     */
    public const FREE_SERVICES = 1;

    /**
     * How many services this owner may put photos on.
     *
     * A null / non-positive owner is NOT a free tenant: it is instinctor.com, a
     * paid standalone client install, or DIY WordPress, none of which are on our
     * plans. isPro() returns false for null, so this used to hand them the free
     * cap of one, which both crippled the marquee on client sites and told a
     * paying client "Free sites show one service with photos" (2026-08-13, same
     * family as the lead-alert pitch in Core\Sms).
     */
    public static function maxServices(?int $ownerId = null): int
    {
        $ownerId = $ownerId ?? (class_exists('\Instinctor\Core\Tenant')
            ? (int) \Instinctor\Core\Tenant::currentOwnerId() : 0);
        if ((int) $ownerId <= 0) return self::MAX_SERVICES;
        return \Instinctor\Core\TenantPlan::isPro((int) $ownerId) ? self::MAX_SERVICES : self::FREE_SERVICES;
    }

    /**
     * Replace one service's photos. Returns [ok, message, storedCount].
     *
     * Refuses rather than truncates when the owner is over a limit, because silently
     * dropping somebody's photos and reporting success is how you get a support ticket
     * that nobody can reproduce.
     */
    public static function set(int $ownerId, int $serviceId, array $urls): array
    {
        if ($ownerId <= 0 || $serviceId <= 0) return [false, 'Unknown service.', 0];

        $p   = DB::prefix();
        $own = DB::get_row(DB::prepare(
            "SELECT id FROM {$p}instinctor_services WHERE id = %d AND "
            . Tenant::scopeSqlFor($ownerId, 'owner_id') . " LIMIT 1",
            $serviceId
        ));
        if (!$own) return [false, 'That service is not yours.', 0];

        $clean = self::clean($urls);
        if (count($clean) > self::MAX_PER_SERVICE) {
            return [false, 'Up to ' . self::MAX_PER_SERVICE . ' photos per service.', 0];
        }

        // How many OTHER services already have photos? Four strips is the ceiling, so
        // the fifth service to be given photos is refused rather than rendered and
        // silently ignored by the marquee.
        if ($clean) {
            $others = (int) DB::get_var(DB::prepare(
                "SELECT COUNT(*) FROM {$p}instinctor_services
                  WHERE id <> %d AND status = 'active'
                    AND photos IS NOT NULL AND photos <> '' AND photos <> '[]'
                    AND " . Tenant::scopeSqlFor($ownerId, 'owner_id'),
                $serviceId
            ));
            $allowed = self::maxServices($ownerId);
            if ($others >= $allowed) {
                return [false, $allowed === 1
                    ? 'Free sites show one service with photos. Upgrade to show all of your work.'
                    : 'Photos can go on up to ' . $allowed . ' services. Remove them from another service first.', 0];
            }
        }

        DB::update(
            $p . 'instinctor_services',
            ['photos' => $clean ? wp_json_encode(array_values($clean)) : null],
            ['id' => $serviceId]
        );
        return [true, '', count($clean)];
    }

    /**
     * Keep only images this site actually serves.
     *
     * Same rule as the renderer: storage-relative, or absolute on our own host. An
     * off-site URL is not merely untidy, it hotlinks a stranger's server from a page
     * we are responsible for, and it survives long after they delete the file.
     */
    public static function clean(array $urls): array
    {
        $mine = strtolower((string) parse_url(
            (string) (function_exists('home_url') ? home_url('/') : ''), PHP_URL_HOST
        ));
        $out = [];
        foreach ($urls as $u) {
            $u = trim((string) (is_array($u) ? ($u['url'] ?? '') : $u));
            if ($u === '') continue;
            if ($u[0] !== '/') {
                $host = strtolower((string) parse_url($u, PHP_URL_HOST));
                if ($host === '' || $mine === '' || $host !== $mine) continue;
            }
            if (!preg_match('/\.(jpe?g|png|webp|gif|avif)(\?|$)/i', $u)) continue;
            if (in_array($u, $out, true)) continue;      // the same photo twice is a mistake
            $out[] = $u;
        }
        return $out;
    }

    /** Photos currently on a service, already filtered. */
    public static function get(int $ownerId, int $serviceId): array
    {
        $p = DB::prefix();
        $row = DB::get_row(DB::prepare(
            "SELECT photos FROM {$p}instinctor_services WHERE id = %d AND "
            . Tenant::scopeSqlFor($ownerId, 'owner_id') . " LIMIT 1",
            $serviceId
        ));
        if (!$row) return [];
        $raw = json_decode((string) ($row->photos ?? ''), true);
        return is_array($raw) ? array_slice(self::clean($raw), 0, self::MAX_PER_SERVICE) : [];
    }
}
