<?php
/**
 * ApiTokens — bearer token issue/verify/revoke for the mobile app.
 *
 * Token format: 64-char hex from random_bytes(32). Only the SHA-256 hash is
 * stored server-side; the raw token is returned once on issue and lives on
 * the device from then on. Rolling TTL — every use bumps last_used_at,
 * expiration is `last_used_at + TTL_DAYS`, so an active driver never has to
 * re-login. An inactive one gets kicked after 30 days silence.
 */
class ApiTokens extends Model
{
    private const TTL_DAYS = 30;

    /**
     * Issue a fresh token for a driver. Returns [token, tokenId, expiresAt].
     */
    public function issue(int $driverId, array $ctx = []): array
    {
        $raw     = bin2hex(random_bytes(32));
        $hash    = hash('sha256', $raw);
        $expires = date('Y-m-d H:i:s', time() + (self::TTL_DAYS * 86400));

        $r = (new DB())->insertUpdate(
            "INSERT INTO api_tokens
                (tenant_id, driver_id, token_hash, device_label, device_platform, app_version,
                 ip_address, user_agent, last_used_at, expires_at, created_on)
             VALUES
                (:tenant, :did, :h, :dl, :dp, :av,
                 :ip, :ua, NOW(), :exp, NOW())",
            [
                ':tenant' => defined('DEFAULT_TENANT_ID') ? DEFAULT_TENANT_ID : 1,
                ':did'    => $driverId,
                ':h'      => $hash,
                ':dl'     => $ctx['device_label']    ?? null,
                ':dp'     => $ctx['device_platform'] ?? null,
                ':av'     => $ctx['app_version']     ?? null,
                ':ip'     => $ctx['ip']  ?? null,
                ':ua'     => $ctx['ua']  ?? null,
                ':exp'    => $expires,
            ]
        );

        return ['token' => $raw, 'token_id' => (int)$r['last_id'], 'expires_at' => $expires];
    }

    /**
     * Look up a token, verify it's alive, and return the driver row on success.
     * Also touches last_used_at + expires_at (rolling TTL).
     * Returns null on any failure (expired, revoked, unknown).
     */
    public function verifyAndTouch(string $rawToken): ?array
    {
        if ($rawToken === '' || strlen($rawToken) !== 64) return null;
        $hash = hash('sha256', $rawToken);

        $db = new DB();
        $rows = $db->mySelect(
            "SELECT t.c0d AS token_id, t.driver_id, t.expires_at, t.revoked_at, t.tenant_id,
                    d.full_name, d.phone_1, d.employee_no, d.email, d.status AS driver_status,
                    d.mobile_login_enabled, d.photo_url, d.licence_no, d.licence_class, d.licence_expires
               FROM api_tokens t
          LEFT JOIN drivers d ON d.c0d = t.driver_id
              WHERE t.token_hash = :h LIMIT 1",
            [':h' => $hash]
        );
        $row = $rows[0] ?? null;
        if (!$row) return null;
        if ($row['revoked_at'] !== null) return null;
        if (strtotime($row['expires_at']) < time()) return null;
        if ((int)$row['mobile_login_enabled'] !== 1) return null;
        if ($row['driver_status'] === 'suspended' || $row['driver_status'] === 'terminated') return null;

        // Rolling TTL — bump last_used + expires
        $newExpires = date('Y-m-d H:i:s', time() + (self::TTL_DAYS * 86400));
        $db->insertUpdate(
            "UPDATE api_tokens SET last_used_at = NOW(), expires_at = :e WHERE c0d = :id",
            [':e' => $newExpires, ':id' => $row['token_id']]
        );

        return [
            'token_id'  => (int)$row['token_id'],
            'driver_id' => (int)$row['driver_id'],
            'tenant_id' => (int)$row['tenant_id'],
            'driver'    => [
                'id'               => (int)$row['driver_id'],
                'full_name'        => $row['full_name'],
                'phone'            => $row['phone_1'],
                'employee_no'      => $row['employee_no'],
                'email'            => $row['email'],
                'photo_url'        => $row['photo_url'],
                'licence_no'       => $row['licence_no'],
                'licence_class'    => $row['licence_class'],
                'licence_expires'  => $row['licence_expires'],
            ],
        ];
    }

    public function revoke(int $tokenId): void
    {
        (new DB())->insertUpdate(
            "UPDATE api_tokens SET revoked_at = NOW() WHERE c0d = :id AND revoked_at IS NULL",
            [':id' => $tokenId]
        );
    }

    public function revokeAllForDriver(int $driverId): void
    {
        (new DB())->insertUpdate(
            "UPDATE api_tokens SET revoked_at = NOW() WHERE driver_id = :did AND revoked_at IS NULL",
            [':did' => $driverId]
        );
    }

    /**
     * Look up a driver row by phone (E.164 or local) + verify PIN. Returns
     * driver_id on success or 0 on failure.
     */
    public function authenticateByPhonePin(string $phone, string $pin): int
    {
        $phone = trim($phone);
        if ($phone === '' || $pin === '') return 0;

        // Match phone flexibly: as-typed, without leading '+', and with leading '0' local variant
        $variants = [$phone];
        if (str_starts_with($phone, '+255')) $variants[] = '0' . substr($phone, 4);
        if (str_starts_with($phone, '255'))  $variants[] = '0' . substr($phone, 3);
        if (str_starts_with($phone, '0'))    $variants[] = '+255' . substr($phone, 1);
        $variants = array_values(array_unique(array_filter($variants)));

        $placeholders = implode(',', array_fill(0, count($variants), '?'));
        $rs = (new DB())->mySelect(
            "SELECT c0d, mobile_password_hash, mobile_login_enabled, status
               FROM drivers
              WHERE deleted = 0 AND phone_1 IN ({$placeholders})
              LIMIT 1",
            $variants
        );
        $d = $rs[0] ?? null;
        if (!$d)                                  return 0;
        if ((int)$d['mobile_login_enabled'] !== 1) return 0;
        if (!$d['mobile_password_hash'])          return 0;
        if (!password_verify($pin, $d['mobile_password_hash'])) return 0;

        (new DB())->insertUpdate(
            "UPDATE drivers SET mobile_last_login_at = NOW() WHERE c0d = :id",
            [':id' => (int)$d['c0d']]
        );
        return (int)$d['c0d'];
    }

    /** Housekeeping — called from cron/nightly. Deletes expired-and-revoked tokens > 90d old. */
    public function purgeStale(): int
    {
        $r = (new DB())->insertUpdate(
            "DELETE FROM api_tokens WHERE (revoked_at IS NOT NULL AND revoked_at < NOW() - INTERVAL 90 DAY)
                                       OR (expires_at < NOW() - INTERVAL 90 DAY)", []
        );
        return (int)$r['success'];
    }

    public function log(?int $driverId, ?int $tokenId, string $endpoint, string $method, int $status, ?string $err, int $ms): void
    {
        (new DB())->insertUpdate(
            "INSERT INTO api_audit_log (driver_id, token_id, endpoint, http_method, http_status, ip_address, user_agent, error_msg, duration_ms, created_on)
             VALUES (:d, :t, :e, :m, :s, :ip, :ua, :err, :ms, NOW())",
            [
                ':d' => $driverId, ':t' => $tokenId,
                ':e' => mb_substr($endpoint, 0, 60), ':m' => $method, ':s' => $status,
                ':ip' => mb_substr($_SERVER['REMOTE_ADDR'] ?? '', 0, 45),
                ':ua' => mb_substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
                ':err' => $err ? mb_substr($err, 0, 255) : null,
                ':ms' => $ms,
            ]
        );
    }
}
