<?php
declare(strict_types=1);

namespace Instinctor\Services;

use Instinctor\Core\DB;
use Instinctor\Core\Config;
use Instinctor\Core\TenantPlan;

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

/**
 * Free-site lifecycle: keep abandoned free sites out of Google (and off the
 * expiry clock's wrong side) until they show a sign of life.
 *
 * A free tenant site is a site_owner account's published page(s) on the managed
 * app, with the owner NOT on Pro. Fresh from /demo/publish-free it is a real
 * public instinctor.com/<slug> page, but if the visitor was just wandering it may
 * never be touched again. Indexing thousands of those would bloat the sitemap and
 * drag the whole domain down with thin, near-duplicate content.
 *
 * So a free tenant site is noindex + out of the sitemap UNTIL it is "activated":
 * it received a lead / booking / chat, earned real human traffic, or the owner
 * came back (a later login, or an edit — see the activation hooks). The flag is
 * sticky (once alive, always indexed) and lives in owner config.
 *
 * EVERYTHING here must fail SAFE: shouldIndexPage() runs on every page render, so
 * any error defaults to "index" rather than breaking the page or hiding a real
 * site. A missing optional table (estimates/bookings/chat exist only where wired)
 * counts as "no signal", never a fatal.
 */
final class SiteLifecycle
{
    /** Sticky "this site is alive" flag + why + when (owner-scoped config). */
    public const FLAG_ACTIVATED = 'instinctor_site_activated';
    public const FLAG_REASON    = 'instinctor_site_activated_reason';
    public const FLAG_AT        = 'instinctor_site_activated_at';

    /** Verified human pageviews that count as "real people found it". */
    public const MIN_VIEWS = 3;

    /** Per-request memo so a render/sitemap pass resolves each owner once. */
    private static array $activeCache = [];
    private static array $freeCache   = [];

    /** A free tenant = their OWN site_owner account, on the managed app, not on Pro. */
    public static function isFreeTenantOwner(int $ownerId): bool
    {
        if ($ownerId <= 0) return false;
        if (isset(self::$freeCache[$ownerId])) return self::$freeCache[$ownerId];
        try {
            $u    = DB::prefix() . 'instinctor_users';
            $role = (string) DB::get_var(DB::prepare("SELECT role FROM {$u} WHERE id = %d", $ownerId));
            $free = ($role === 'site_owner') && !TenantPlan::isPro($ownerId);
        } catch (\Throwable $e) {
            $free = false;   // unknown -> treat as not-a-free-tenant, so it indexes
        }
        return self::$freeCache[$ownerId] = $free;
    }

    /** Has the owner's site shown life? Sticky: once true, stays true. */
    public static function isActivated(int $ownerId): bool
    {
        if ($ownerId <= 0) return false;
        if (isset(self::$activeCache[$ownerId])) return self::$activeCache[$ownerId];
        try {
            if ((string) Config::ownerOnly($ownerId, self::FLAG_ACTIVATED, '0') === '1') {
                return self::$activeCache[$ownerId] = true;
            }
            $reason = self::computeSignal($ownerId);
            if ($reason !== null) {
                self::markActivated($ownerId, $reason);      // self-heal: persist so we compute once
                return self::$activeCache[$ownerId] = true;
            }
        } catch (\Throwable $e) {
            return self::$activeCache[$ownerId] = false;
        }
        return self::$activeCache[$ownerId] = false;
    }

    /**
     * May we PITCH this owner yet? Stricter than isActivated(), deliberately.
     *
     * One flag was answering two different questions. "Is this site alive?" (index it,
     * do not remove it) is correctly satisfied by traffic or a return login. "Is this
     * person invested enough to be shown a price?" is not: a stranger landing on their
     * page tells us nothing about the owner, and Sergei's rule is that nothing is
     * offered until they are involved, published AND have edited their site
     * (2026-07-26). Under isActivated() three engaged pageviews were enough to put
     * $29.90 in front of someone who had done nothing but publish.
     *
     * So the pitch needs one of:
     *   edit                    they changed their own site (investment)
     *   lead / booking / chat   the site produced something real (proof it works, and
     *                           the moment Pro is obviously worth it)
     * and NOT: traffic, return_login, restored.
     *
     * Reads the stored reason rather than recomputing, so it stays one query. Never
     * pitches when the reason is unknown: silence is the safe failure here.
     */
    public static function isInvested(int $ownerId): bool
    {
        if ($ownerId <= 0) return false;
        if (!self::isActivated($ownerId)) return false;          // not even alive yet
        try {
            $reason = (string) Config::ownerOnly($ownerId, self::FLAG_REASON, '');
            return in_array($reason, ['edit', 'lead', 'booking', 'chat'], true);
        } catch (\Throwable $e) {
            return false;
        }
    }

    /** The activation signal for this owner, or null if none. Each probe is isolated
     *  so an absent optional table never breaks the whole check. */
    public static function computeSignal(int $ownerId): ?string
    {
        $p = DB::prefix();
        $count = static function (string $sql, int $id): int {
            try { return (int) DB::get_var(DB::prepare($sql, $id)); } catch (\Throwable $e) { return 0; }
        };

        // Inbound activity: a lead, a booking, or a chat means the site did something.
        if ($count("SELECT COUNT(*) FROM {$p}instinctor_estimates WHERE owner_id = %d", $ownerId) > 0)          return 'lead';
        if ($count("SELECT COUNT(*) FROM {$p}instinctor_bookings WHERE owner_id = %d", $ownerId) > 0)           return 'booking';
        if ($count("SELECT COUNT(*) FROM {$p}instinctor_chat_conversations WHERE owner_id = %d", $ownerId) > 0) return 'chat';

        // Real human traffic (verified: not a bot, beaconed, engaged >= 2s).
        $views = $count(
            "SELECT COUNT(*) FROM {$p}instinctor_pageviews v
               JOIN {$p}instinctor_pages pg ON pg.id = v.page_id
              WHERE pg.owner_id = %d AND v.is_bot = 0 AND v.beaconed = 1 AND v.engaged_seconds >= 2",
            $ownerId);
        if ($views >= self::MIN_VIEWS) return 'traffic';

        // Owner came back: an auth session created > 1 day after the account (the
        // publish itself mints the first session, so a later one is a real return).
        try {
            $created = (string) DB::get_var(DB::prepare("SELECT created_at FROM {$p}instinctor_users WHERE id = %d", $ownerId));
            if ($created !== '') {
                $ret = (int) DB::get_var(DB::prepare(
                    "SELECT COUNT(*) FROM {$p}instinctor_sessions
                      WHERE user_id = %d AND type = 'auth' AND created_at > DATE_ADD(%s, INTERVAL 1 DAY)",
                    $ownerId, $created));
                if ($ret > 0) return 'return_login';
            }
        } catch (\Throwable $e) { /* no session table / no signal */ }

        return null;
    }

    /** Flag the owner activated (idempotent — the first reason wins). */
    public static function markActivated(int $ownerId, string $reason): void
    {
        if ($ownerId <= 0) return;
        try {
            if ((string) Config::ownerOnly($ownerId, self::FLAG_ACTIVATED, '0') === '1') {
                self::$activeCache[$ownerId] = true;
                return;
            }
            Config::setForOwner($ownerId, self::FLAG_ACTIVATED, '1');
            Config::setForOwner($ownerId, self::FLAG_REASON, $reason);
            Config::setForOwner($ownerId, self::FLAG_AT, gmdate('c'));
            self::$activeCache[$ownerId] = true;
        } catch (\Throwable $e) { /* never let activation-marking break a request */ }
    }

    /**
     * Should this page be indexed (sitemap + robots)? Everything indexes EXCEPT a
     * free tenant site whose owner has not shown life yet. Vendor/marketing pages
     * (no owner) and Pro / activated tenants always index. Fails safe to true.
     *
     * @param int      $pageId  the page id
     * @param int|null $ownerId pass the row's owner_id when you already have it
     *                          (render path) to skip the lookup; null looks it up.
     */
    public static function shouldIndexPage(int $pageId, ?int $ownerId = null): bool
    {
        try {
            if ($ownerId === null) {
                $ownerId = (int) (PageService::ownerOf($pageId) ?? 0);
            }
            if ($ownerId <= 0) return true;                       // vendor / unowned
            if (!self::isFreeTenantOwner($ownerId)) return true;  // Pro or non-tenant
            return self::isActivated($ownerId);                   // free tenant: only if alive
        } catch (\Throwable $e) {
            return true;   // never hide a page (or break a render) on an error here
        }
    }
}
