<?php
#################################################################################
##              -= YOU MAY NOT REMOVE OR CHANGE THIS NOTICE =-                 ##
## --------------------------------------------------------------------------- ##
##  Filename       : PushProtection.php                                        ##
##  Type           : Push-Protection engine (Admin/Multihunter dashboard)      ##
## --------------------------------------------------------------------------- ##
##  Developed by   : Shadow 		                                           ##
##  Project        : TravianZ                                                  ##
##  GitHub         : https://github.com/Shadowss/TravianZ                      ##
## --------------------------------------------------------------------------- ##
##  License        : TravianZ Project                                          ##
##  Copyright      : TravianZ (c) 2010-2026. All rights reserved.              ##
## --------------------------------------------------------------------------- ##
#################################################################################

/**
 * PushProtection
 * -------------------------------------------------------------------------
 * Visibility + control layer over inter-player resource transfers, modelled
 * on Travian Legends' pushing-protection rules:
 *
 *   - Every COMPLETED marketplace delivery between DIFFERENT players is logged
 *     to resource_transfer_log (hook in Automation::marketCompleteDeliveries).
 *   - For each receiving player we compute, over a rolling 7-day window, the
 *     total resources RECEIVED FROM OTHERS ("7-day balance").
 *   - We compute an automatic limit from the player's hourly production
 *     ("hours of production" model: limit = HOURS_ALLOWED * net hourly prod).
 *   - Exceptions (like TL): WW villages may be supplied without limit; artefact
 *     villages are crop-supply exceptions. These are surfaced as suggested
 *     overrides, applied by an admin with one click (push_override table).
 *
 * This is a DASHBOARD + override store. It does not block transfers by itself;
 * an optional enforcement hook (allowedToReceive) is provided for the market
 * send flow if you later want hard enforcement.
 *
 * Self-contained (static, resolves DB link from globals) so both Automation
 * (logging) and the admin panel (dashboard) can use it without wiring.
 */
class PushProtection
{
    /* ---- Tunables ------------------------------------------------------ */

    /** Rolling balance window, in days (TL uses 7). */
    const WINDOW_DAYS = 7;

    /** Aggregate allowance: how many HOURS of the receiver's own production may
     *  be received from other players within the window. TL varies this by
     *  relationship (1 / 14 / 21h); this single aggregate cap is a moderation
     *  simplification — tune to taste. */
    const HOURS_ALLOWED = 24;

    /** Percent-of-limit at which a player is flagged "near limit". */
    const NEAR_LIMIT_PCT = 80;

    /** Override modes stored in push_override.mode. */
    const OV_NONE      = 0; // no override -> automatic limit applies
    const OV_UNLIMITED = 1; // exempt (WW villages, trusted) -> no limit
    const OV_CUSTOM    = 3; // custom_limit is an absolute cap (resources / window)

    /* ---- DB plumbing --------------------------------------------------- */

    private static function link()
    {
        if (isset($GLOBALS['link']) && $GLOBALS['link']) {
            return $GLOBALS['link'];
        }
        if (isset($GLOBALS['database']) && isset($GLOBALS['database']->dblink)) {
            return $GLOBALS['database']->dblink;
        }
        return null;
    }

    /** Create both tables if missing (lazy — works without re-running installer). */
    public static function ensureSchema()
    {
        $link = self::link();
        if (!$link) {
            return;
        }
        @mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "resource_transfer_log` (
            `id`        int(11) NOT NULL AUTO_INCREMENT,
            `from_vref` int(11) NOT NULL DEFAULT 0,
            `to_vref`   int(11) NOT NULL DEFAULT 0,
            `from_uid`  int(11) NOT NULL DEFAULT 0,
            `to_uid`    int(11) NOT NULL DEFAULT 0,
            `wood`      int(11) NOT NULL DEFAULT 0,
            `clay`      int(11) NOT NULL DEFAULT 0,
            `iron`      int(11) NOT NULL DEFAULT 0,
            `crop`      int(11) NOT NULL DEFAULT 0,
            `time`      int(11) NOT NULL DEFAULT 0,
            PRIMARY KEY (`id`),
            KEY `to_uid_time` (`to_uid`,`time`),
            KEY `from_uid_time` (`from_uid`,`time`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8");

        @mysqli_query($link, "CREATE TABLE IF NOT EXISTS `" . TB_PREFIX . "push_override` (
            `uid`          int(11) NOT NULL,
            `mode`         tinyint(2) NOT NULL DEFAULT 0,
            `custom_limit` bigint(20) NOT NULL DEFAULT 0,
            `note`         varchar(255) NOT NULL DEFAULT '',
            `set_by`       int(11) NOT NULL DEFAULT 0,
            `time`         int(11) NOT NULL DEFAULT 0,
            PRIMARY KEY (`uid`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8");
    }

    /* ---- Data collection (called from Automation on delivery) ---------- */

    /**
     * Log one completed cross-player resource delivery. Best-effort; never
     * throws. Skips same-owner transfers and system accounts.
     */
    public static function logTransfer($fromVref, $toVref, $fromUid, $toUid,
        $wood, $clay, $iron, $crop, $time = 0)
    {
        try {
            $fromUid = (int) $fromUid;
            $toUid   = (int) $toUid;
            if ($fromUid === $toUid || $fromUid <= 3 || $toUid <= 3) {
                return; // own transfer or system account
            }
            $wood = (int) $wood; $clay = (int) $clay; $iron = (int) $iron; $crop = (int) $crop;
            if (($wood + $clay + $iron + $crop) <= 0) {
                return;
            }
            $link = self::link();
            if (!$link) {
                return;
            }
            self::ensureSchema();

            $fromVref = (int) $fromVref;
            $toVref   = (int) $toVref;
            $time     = $time > 0 ? (int) $time : time();

            $stmt = mysqli_prepare($link,
                "INSERT INTO `" . TB_PREFIX . "resource_transfer_log`
                 (from_vref, to_vref, from_uid, to_uid, wood, clay, iron, crop, `time`)
                 VALUES (?,?,?,?,?,?,?,?,?)");
            if (!$stmt) {
                return;
            }
            mysqli_stmt_bind_param($stmt, 'iiiiiiiii',
                $fromVref, $toVref, $fromUid, $toUid, $wood, $clay, $iron, $crop, $time);
            mysqli_stmt_execute($stmt);
            mysqli_stmt_close($stmt);
        } catch (\Throwable $e) {
            // never break the automation cron
        }
    }

    /* ---- Overrides ----------------------------------------------------- */

    /** All overrides as uid => ['mode','custom_limit','note','set_by','time']. */
    public static function overrides()
    {
        $link = self::link();
        $out = [];
        if (!$link) {
            return $out;
        }
        self::ensureSchema();
        $r = @mysqli_query($link, "SELECT * FROM `" . TB_PREFIX . "push_override`");
        if ($r) {
            while ($row = mysqli_fetch_assoc($r)) {
                $out[(int) $row['uid']] = $row;
            }
            mysqli_free_result($r);
        }
        return $out;
    }

    /** Insert/update or clear an override. mode OV_NONE removes it. */
    public static function setOverride($uid, $mode, $customLimit, $note, $adminId)
    {
        $link = self::link();
        if (!$link) {
            return false;
        }
        self::ensureSchema();

        $uid    = (int) $uid;
        $mode   = (int) $mode;
        $custom = (int) $customLimit;
        $admin  = (int) $adminId;
        $now    = time();

        if ($uid <= 3) {
            return false;
        }

        if ($mode === self::OV_NONE) {
            $stmt = mysqli_prepare($link,
                "DELETE FROM `" . TB_PREFIX . "push_override` WHERE uid = ?");
            if (!$stmt) { return false; }
            mysqli_stmt_bind_param($stmt, 'i', $uid);
            $ok = mysqli_stmt_execute($stmt);
            mysqli_stmt_close($stmt);
            return $ok;
        }

        $note = substr((string) $note, 0, 255);
        $stmt = mysqli_prepare($link,
            "INSERT INTO `" . TB_PREFIX . "push_override` (uid, mode, custom_limit, note, set_by, `time`)
             VALUES (?,?,?,?,?,?)
             ON DUPLICATE KEY UPDATE mode=VALUES(mode), custom_limit=VALUES(custom_limit),
                                     note=VALUES(note), set_by=VALUES(set_by), `time`=VALUES(`time`)");
        if (!$stmt) { return false; }
        mysqli_stmt_bind_param($stmt, 'iiisii', $uid, $mode, $custom, $note, $admin, $now);
        $ok = mysqli_stmt_execute($stmt);
        mysqli_stmt_close($stmt);
        return $ok;
    }

    /* ---- Exception sets (WW / artefact villages) ----------------------- */

    /** Owners holding at least one WW village (fdata.f99t = 40 = WONDER). */
    public static function wwOwners()
    {
        $link = self::link();
        $out = [];
        if (!$link) { return $out; }
        $r = @mysqli_query($link,
            "SELECT DISTINCT v.owner
             FROM `" . TB_PREFIX . "vdata` v
             JOIN `" . TB_PREFIX . "fdata` f ON v.wref = f.vref
             WHERE f.f99t = 40 AND v.owner > 3");
        if ($r) {
            while ($row = mysqli_fetch_row($r)) { $out[(int) $row[0]] = true; }
            mysqli_free_result($r);
        }
        return $out;
    }

    /** Owners holding at least one active artefact. */
    public static function artefactOwners()
    {
        $link = self::link();
        $out = [];
        if (!$link) { return $out; }
        $r = @mysqli_query($link,
            "SELECT DISTINCT owner FROM `" . TB_PREFIX . "artefacts`
             WHERE active = 1 AND (del = 0 OR del IS NULL) AND owner > 3");
        if ($r) {
            while ($row = mysqli_fetch_row($r)) { $out[(int) $row[0]] = true; }
            mysqli_free_result($r);
        }
        return $out;
    }

    /* ---- Production model ---------------------------------------------- */

    /**
     * Net hourly production for a whole account (sum of all villages).
     * Wood+Clay+Iron + max(0, Crop - population). Uses the same field/factory
     * math as Village.php (base fields + factory %, x SPEED), but omits oasis /
     * hero / plus bonuses (not resolvable in a batch context) — a documented,
     * slightly conservative approximation. Returns int resources/hour.
     */
    public static function hourlyProduction($uid, $link = null)
    {
        $link = $link ?: self::link();
        if (!$link) { return 0; }

        $uid = (int) $uid;
        $total = 0;
        $r = @mysqli_query($link,
            "SELECT f.*, v.pop
             FROM `" . TB_PREFIX . "fdata` f
             JOIN `" . TB_PREFIX . "vdata` v ON v.wref = f.vref
             WHERE v.owner = " . $uid);
        if (!$r) { return 0; }
        while ($f = mysqli_fetch_assoc($r)) {
            list($w, $c, $i, $cr) = self::villageProd($f);
            $pop = (int) ($f['pop'] ?? 0);
            $cropNet = max(0, $cr - $pop);
            $total += $w + $c + $i + $cropNet;
        }
        mysqli_free_result($r);
        return (int) $total;
    }

    /** Per-village production [wood,clay,iron,crop] per hour from an fdata row. */
    private static function villageProd(array $f)
    {
        global $bid1, $bid2, $bid3, $bid4, $bid5, $bid6, $bid7, $bid8, $bid9;
        if (!isset($bid1)) {
            return [0, 0, 0, 0]; // buidata not loaded in this context
        }
        $wood = $clay = $iron = $crop = 0;
        $saw = $brick = $found = $mill = $bake = 0;

        for ($i = 1; $i <= 38; $i++) {
            $t  = (int) ($f["f{$i}t"] ?? 0);
            $lv = (int) ($f["f{$i}"] ?? 0);
            if     ($t === 1) { $wood += $bid1[$lv]['prod'] ?? 0; }
            elseif ($t === 2) { $clay += $bid2[$lv]['prod'] ?? 0; }
            elseif ($t === 3) { $iron += $bid3[$lv]['prod'] ?? 0; }
            elseif ($t === 4) { $crop += $bid4[$lv]['prod'] ?? 0; }
            elseif ($t === 5) { $saw   = $lv; }
            elseif ($t === 6) { $brick = $lv; }
            elseif ($t === 7) { $found = $lv; }
            elseif ($t === 8) { $mill  = $lv; }
            elseif ($t === 9) { $bake  = $lv; }
        }
        if ($saw)   { $wood += $wood / 100 * ($bid5[$saw]['attri'] ?? 0); }
        if ($brick) { $clay += $clay / 100 * ($bid6[$brick]['attri'] ?? 0); }
        if ($found) { $iron += $iron / 100 * ($bid7[$found]['attri'] ?? 0); }
        if ($mill || $bake) {
            $crop += $crop / 100 * (($bid8[$mill]['attri'] ?? 0) + ($bid9[$bake]['attri'] ?? 0));
        }
        $spd = defined('SPEED') ? SPEED : 1;
        return [
            (int) round($wood * $spd),
            (int) round($clay * $spd),
            (int) round($iron * $spd),
            (int) round($crop * $spd),
        ];
    }

    /* ---- 7-day balance ------------------------------------------------- */

    /**
     * Total resources each player RECEIVED FROM OTHERS in the window, keyed by
     * to_uid. Returns uid => ['total','wood','clay','iron','crop','senders'].
     */
    public static function receivedFromOthers($sinceTs, $link = null)
    {
        $link = $link ?: self::link();
        $out = [];
        if (!$link) { return $out; }
        self::ensureSchema();

        $r = @mysqli_query($link,
            "SELECT to_uid,
                    SUM(wood) w, SUM(clay) c, SUM(iron) i, SUM(crop) cr,
                    SUM(wood+clay+iron+crop) tot,
                    COUNT(DISTINCT from_uid) senders
             FROM `" . TB_PREFIX . "resource_transfer_log`
             WHERE `time` >= " . (int) $sinceTs . " AND from_uid <> to_uid
             GROUP BY to_uid");
        if ($r) {
            while ($row = mysqli_fetch_assoc($r)) {
                $out[(int) $row['to_uid']] = [
                    'total'   => (int) $row['tot'],
                    'wood'    => (int) $row['w'],
                    'clay'    => (int) $row['c'],
                    'iron'    => (int) $row['i'],
                    'crop'    => (int) $row['cr'],
                    'senders' => (int) $row['senders'],
                ];
            }
            mysqli_free_result($r);
        }
        return $out;
    }

    /* ---- Dashboard ----------------------------------------------------- */

    /**
     * Build the push-protection dashboard rows.
     *
     * @param array $opts 'days','hours','only' ('all'|'over'|'flagged').
     * @return array ['rows'=>..., 'window_days'=>..., 'hours_allowed'=>...]
     */
    public static function dashboard(array $opts = [])
    {
        $link = self::link();
        if (!$link) {
            return ['rows' => [], 'window_days' => self::WINDOW_DAYS, 'hours_allowed' => self::HOURS_ALLOWED];
        }
        self::ensureSchema();

        $days  = isset($opts['days'])  ? max(1, (int) $opts['days'])  : self::WINDOW_DAYS;
        $hours = isset($opts['hours']) ? max(1, (int) $opts['hours']) : self::HOURS_ALLOWED;
        $only  = $opts['only'] ?? 'all';
        $since = time() - $days * 86400;

        $received  = self::receivedFromOthers($since, $link);
        $overrides = self::overrides();
        $ww        = self::wwOwners();
        $arte      = self::artefactOwners();

        // Candidate set: anyone who received from others, plus flagged/override.
        $uids = [];
        foreach (array_keys($received)  as $u) { $uids[$u] = true; }
        foreach (array_keys($overrides) as $u) { $uids[$u] = true; }
        foreach (array_keys($ww)        as $u) { $uids[$u] = true; }
        foreach (array_keys($arte)      as $u) { $uids[$u] = true; }
        unset($uids[0]);

        if (empty($uids)) {
            return ['rows' => [], 'window_days' => $days, 'hours_allowed' => $hours];
        }

        // Names / pop / village count in one pass.
        $in = implode(',', array_map('intval', array_keys($uids)));
        $meta = [];
        $r = @mysqli_query($link,
            "SELECT u.id, u.username,
                    (SELECT COUNT(*) FROM `" . TB_PREFIX . "vdata` v WHERE v.owner = u.id) AS villages,
                    (SELECT COALESCE(SUM(v2.pop),0) FROM `" . TB_PREFIX . "vdata` v2 WHERE v2.owner = u.id) AS pop
             FROM `" . TB_PREFIX . "users` u WHERE u.id IN ($in)");
        if ($r) {
            while ($row = mysqli_fetch_assoc($r)) {
                $meta[(int) $row['id']] = $row;
            }
            mysqli_free_result($r);
        }

        $rows = [];
        foreach (array_keys($uids) as $uid) {
            $uid = (int) $uid;
            if ($uid <= 3 || !isset($meta[$uid])) {
                continue;
            }
            $rec  = $received[$uid]['total'] ?? 0;
            $prod = self::hourlyProduction($uid, $link);
            $autoLimit = $hours * $prod;

            $isWW   = isset($ww[$uid]);
            $isArte = isset($arte[$uid]);
            $ov     = $overrides[$uid] ?? null;

            // Effective limit + status.
            $unlimited = false;
            $effLimit  = $autoLimit;
            $ovLabel   = '';
            if ($ov) {
                $mode = (int) $ov['mode'];
                if ($mode === self::OV_UNLIMITED) {
                    $unlimited = true;
                    $ovLabel = 'Exempt (manual)';
                } elseif ($mode === self::OV_CUSTOM) {
                    $effLimit = (int) $ov['custom_limit'];
                    $ovLabel = 'Custom cap';
                }
            }

            if ($unlimited) {
                $pct = 0;
                $status = 'Exempt';
            } elseif ($effLimit <= 0) {
                $pct = $rec > 0 ? 999 : 0;
                $status = $rec > 0 ? 'Over' : 'OK';
            } else {
                $pct = (int) round($rec / $effLimit * 100);
                $status = $pct > 100 ? 'Over' : ($pct >= self::NEAR_LIMIT_PCT ? 'Near' : 'OK');
            }

            $flags = [];
            if ($isWW)   { $flags[] = 'WW village'; }
            if ($isArte) { $flags[] = 'Artefact village'; }

            $row = [
                'uid'        => $uid,
                'name'       => $meta[$uid]['username'],
                'villages'   => (int) $meta[$uid]['villages'],
                'pop'        => (int) $meta[$uid]['pop'],
                'prod_h'     => $prod,
                'auto_limit' => $autoLimit,
                'eff_limit'  => $unlimited ? -1 : $effLimit, // -1 = unlimited
                'received'   => $rec,
                'senders'    => $received[$uid]['senders'] ?? 0,
                'pct'        => $pct,
                'status'     => $status,
                'is_ww'      => $isWW,
                'is_arte'    => $isArte,
                'ov_mode'    => $ov ? (int) $ov['mode'] : self::OV_NONE,
                'ov_label'   => $ovLabel,
                'ov_note'    => $ov['note'] ?? '',
                'flags'      => $flags,
            ];

            if ($only === 'over' && $status !== 'Over') { continue; }
            if ($only === 'flagged' && !$isWW && !$isArte && !$ov) { continue; }

            $rows[] = $row;
        }

        // Sort: Over first, then by pct desc, then received desc.
        usort($rows, function ($a, $b) {
            $rank = ['Over' => 0, 'Near' => 1, 'OK' => 2, 'Exempt' => 3];
            $ra = $rank[$a['status']] ?? 4;
            $rb = $rank[$b['status']] ?? 4;
            if ($ra !== $rb) { return $ra <=> $rb; }
            if ($a['pct'] !== $b['pct']) { return $b['pct'] <=> $a['pct']; }
            return $b['received'] <=> $a['received'];
        });

        return ['rows' => $rows, 'window_days' => $days, 'hours_allowed' => $hours];
    }

    /* ---- Optional enforcement hook ------------------------------------ */

    /**
     * OPTIONAL hard-enforcement helper for the market send flow. Returns how
     * many total resources $toUid may still receive from others in the window
     * (PHP_INT_MAX when exempt). Not wired by default — call from Market when
     * you want to actually block over-limit deliveries.
     */
    public static function remainingAllowance($toUid, array $opts = [])
    {
        $link = self::link();
        if (!$link) { return PHP_INT_MAX; }

        $toUid = (int) $toUid;
        $days  = isset($opts['days'])  ? max(1, (int) $opts['days'])  : self::WINDOW_DAYS;
        $hours = isset($opts['hours']) ? max(1, (int) $opts['hours']) : self::HOURS_ALLOWED;
        $since = time() - $days * 86400;

        $ov = self::overrides()[$toUid] ?? null;
        if ($ov && (int) $ov['mode'] === self::OV_UNLIMITED) {
            return PHP_INT_MAX;
        }
        if (isset(self::wwOwners()[$toUid])) {
            return PHP_INT_MAX; // WW villages supplied without limit (TL rule)
        }

        $prod  = self::hourlyProduction($toUid, $link);
        $limit = ($ov && (int) $ov['mode'] === self::OV_CUSTOM)
            ? (int) $ov['custom_limit']
            : $hours * $prod;

        $rec = self::receivedFromOthers($since, $link)[$toUid]['total'] ?? 0;
        return max(0, $limit - $rec);
    }
}
