<?php

namespace App\Services\Whmcs;

use App\Models\Ticket;
use App\Models\WhmcsAdminUser;
use App\Models\WhmcsClientProducts;
use App\Models\WhmcsServer;
use App\Services\AuditLogger;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

/**
 * Keeps the local WHMCS mirror tables (`whmcs_servers`,
 * `whmcs_client_products`, `whmcs_admin_users`) in sync with WHMCS.
 *
 * Two cadences:
 *   - Full sync every 6 h via the scheduler (`syncAll()`).
 *   - Lazy fetch on read miss via `clientProducts($userId)` — drafts
 *     don't have to wait for the next 6 h tick.
 *
 * Change detection: each upsert compares the new payload's hash against
 * the stored row and writes an audit-log entry on any diff. That gives
 * staff a paper trail when a WHMCS admin renames a server / disables a
 * user / etc. without touching the portal.
 */
class WhmcsMirrorSyncer
{
    /** Lazy-fetch TTL — clientProducts() refetches if the row is older. */
    private const CLIENT_TTL_SECONDS = 6 * 3600;

    public function __construct(private WhmcsClient $whmcs) {}

    /**
     * Runs every sync source once. Called by the scheduler every 6 h
     * and by the manual "Sync from WHMCS" button.
     *
     * @return array{servers: array, admins: array, clients: array}
     */
    public function syncAll(): array
    {
        return [
            'servers' => $this->syncServers(),
            'admins'  => $this->syncAdminUsers(),
            'clients' => $this->refreshActiveClients(),
        ];
    }

    // ─────────────────────────── Servers ────────────────────────────

    /**
     * Pulls GetServers, upserts whmcs_servers, logs any field-level diffs.
     * Returns sync stats.
     *
     * @return array{fetched: int, upserted: int, changed: int, removed: int, errors: array}
     */
    public function syncServers(): array
    {
        if (! $this->whmcs->isConfigured()) {
            return ['fetched' => 0, 'upserted' => 0, 'changed' => 0, 'removed' => 0,
                    'errors' => ['whmcs not configured']];
        }

        try {
            $r = $this->whmcs->call('GetServers');
        } catch (\Throwable $e) {
            return ['fetched' => 0, 'upserted' => 0, 'changed' => 0, 'removed' => 0,
                    'errors' => [$e->getMessage()]];
        }
        $rows = $r['servers']['server'] ?? $r['servers'] ?? [];

        $seen = [];
        $changed = $upserted = 0;
        foreach ($rows as $row) {
            $wid = (int) ($row['id'] ?? 0);
            if (! $wid) continue;
            $seen[] = $wid;

            $existing = WhmcsServer::where('whmcs_id', $wid)->first();
            $payload = [
                'whmcs_id'             => $wid,
                'name'                 => (string) ($row['name'] ?? ''),
                'hostname'             => strtolower((string) ($row['hostname'] ?? '')),
                'ipaddress'            => (string) ($row['ipaddress'] ?? ''),
                'module'               => strtolower((string) ($row['module'] ?? '')),
                'active_services'      => (int) ($row['activeServices'] ?? 0),
                'max_allowed_services' => (int) ($row['maxAllowedServices'] ?? 0) ?: null,
                'percent_used'         => isset($row['percentUsed']) ? (int) $row['percentUsed'] : null,
                'raw'                  => $row,
                'last_synced_at'       => now(),
            ];

            if ($existing) {
                $diff = $this->diff($existing->toArray(), $payload, ['raw', 'last_synced_at', 'updated_at', 'created_at']);
                if (! empty($diff)) {
                    $changed++;
                    AuditLogger::log('system', 'whmcs-mirror', 'sync',
                        'whmcs_server.changed', 'whmcs_server', (string) $existing->id,
                        ['whmcs_id' => $wid, 'diff' => $diff]);
                }
                $existing->fill($payload)->save();
            } else {
                $changed++;
                $new = WhmcsServer::create($payload);
                AuditLogger::log('system', 'whmcs-mirror', 'sync',
                    'whmcs_server.added', 'whmcs_server', (string) $new->id,
                    ['whmcs_id' => $wid, 'hostname' => $payload['hostname']]);
            }
            $upserted++;
        }

        // Detect removals — rows in our table that WHMCS no longer returns.
        $removed = 0;
        if (! empty($seen)) {
            $stale = WhmcsServer::whereNotIn('whmcs_id', $seen)->get();
            foreach ($stale as $r) {
                AuditLogger::log('system', 'whmcs-mirror', 'sync',
                    'whmcs_server.removed', 'whmcs_server', (string) $r->id,
                    ['whmcs_id' => $r->whmcs_id, 'hostname' => $r->hostname]);
                $r->delete();
                $removed++;
            }
        }

        // Bust the in-memory cache used by TicketServiceContext until we
        // refactor it to read from the mirror table.
        Cache::forget('hsap.whmcs.servers_by_host');

        return ['fetched' => count($rows), 'upserted' => $upserted,
                'changed' => $changed, 'removed' => $removed, 'errors' => []];
    }

    // ───────────────────────── Admin users ──────────────────────────

    /** @return array{fetched: int, upserted: int, changed: int, errors: array} */
    public function syncAdminUsers(): array
    {
        if (! $this->whmcs->isConfigured()) {
            return ['fetched' => 0, 'upserted' => 0, 'changed' => 0, 'errors' => ['whmcs not configured']];
        }
        try {
            $r = $this->whmcs->call('GetAdminUsers');
        } catch (\Throwable $e) {
            return ['fetched' => 0, 'upserted' => 0, 'changed' => 0, 'errors' => [$e->getMessage()]];
        }
        $rows = $r['admin_users']['admin_user']
              ?? $r['admin_users']
              ?? $r['admins']['admin']
              ?? [];

        $upserted = $changed = 0;
        foreach ($rows as $row) {
            $wid = (int) ($row['id'] ?? 0);
            if (! $wid) continue;
            $full = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? ''));
            $payload = [
                'whmcs_id'       => $wid,
                'username'       => (string) ($row['username'] ?? ''),
                'full_name'      => $full ?: ($row['username'] ?? ''),
                'email'          => (string) ($row['email'] ?? ''),
                'disabled'       => ($row['disabled'] ?? 0) == 1,
                'raw'            => $row,
                'last_synced_at' => now(),
            ];
            $existing = WhmcsAdminUser::where('whmcs_id', $wid)->first();
            if ($existing) {
                $diff = $this->diff($existing->toArray(), $payload, ['raw', 'last_synced_at', 'updated_at', 'created_at']);
                if (! empty($diff)) {
                    $changed++;
                    AuditLogger::log('system', 'whmcs-mirror', 'sync',
                        'whmcs_admin.changed', 'whmcs_admin', (string) $existing->id,
                        ['whmcs_id' => $wid, 'diff' => $diff]);
                }
                $existing->fill($payload)->save();
            } else {
                $changed++;
                WhmcsAdminUser::create($payload);
            }
            $upserted++;
        }
        return ['fetched' => count($rows), 'upserted' => $upserted,
                'changed' => $changed, 'errors' => []];
    }

    // ───────────────────────── Client products ──────────────────────

    /**
     * Read accessor used by TicketServiceContext / NameserverDiagnostic.
     * Returns the cached products array, lazily refetching if missing
     * or stale (older than CLIENT_TTL_SECONDS).
     */
    public function clientProducts(int $userId): array
    {
        if ($userId <= 0) return [];
        $row = WhmcsClientProducts::where('whmcs_user_id', $userId)->first();
        if ($row && $row->last_synced_at
            && $row->last_synced_at->gt(now()->subSeconds(self::CLIENT_TTL_SECONDS))) {
            return $row->products ?? [];
        }
        return $this->fetchAndStoreClient($userId);
    }

    /**
     * Force-refresh ONE client's products. Used by the bulk refresher and
     * the manual sync button.
     */
    public function refreshClient(int $userId): array
    {
        return $this->fetchAndStoreClient($userId);
    }

    /**
     * Refresh every client we've seen a ticket from in the last 30 days
     * AND any cached row older than 24 h regardless. Returns stats.
     *
     * @return array{candidates: int, refreshed: int, errors: int}
     */
    public function refreshActiveClients(int $sinceDays = 30): array
    {
        if (! $this->whmcs->isConfigured()) {
            return ['candidates' => 0, 'refreshed' => 0, 'errors' => 0];
        }

        // Recently-ticketed users.
        $activeIds = Ticket::query()
            ->whereNotNull('whmcs_user_id')
            ->where('whmcs_last_reply_at', '>=', now()->subDays($sinceDays))
            ->distinct()->pluck('whmcs_user_id')->all();

        // Plus any stale cached row (24h+) — covers clients who haven't
        // ticketed but whose data the AI might still reference.
        $staleIds = WhmcsClientProducts::query()
            ->where('last_synced_at', '<=', now()->subHours(24))
            ->pluck('whmcs_user_id')->all();

        $ids = array_values(array_unique(array_merge($activeIds, $staleIds)));
        $refreshed = $errors = 0;
        foreach ($ids as $uid) {
            try {
                $this->fetchAndStoreClient((int) $uid);
                $refreshed++;
            } catch (\Throwable $e) {
                $errors++;
                Log::warning('hsap.whmcs_mirror.client_refresh_failed',
                    ['uid' => $uid, 'err' => $e->getMessage()]);
            }
        }
        return ['candidates' => count($ids), 'refreshed' => $refreshed, 'errors' => $errors];
    }

    private function fetchAndStoreClient(int $userId): array
    {
        try {
            $r = $this->whmcs->call('GetClientsProducts',
                ['clientid' => $userId, 'stats' => false]);
        } catch (\Throwable $e) {
            return [];
        }
        $products = $r['products']['product'] ?? [];

        $existing = WhmcsClientProducts::where('whmcs_user_id', $userId)->first();
        $hashBefore = $existing
            ? md5(json_encode($this->canonicaliseProducts($existing->products ?? [])))
            : null;
        $hashAfter = md5(json_encode($this->canonicaliseProducts($products)));

        $row = WhmcsClientProducts::updateOrCreate(
            ['whmcs_user_id' => $userId],
            [
                'products'       => $products,
                'product_count'  => count($products),
                'last_synced_at' => now(),
            ],
        );

        if ($existing && $hashBefore !== $hashAfter) {
            AuditLogger::log('system', 'whmcs-mirror', 'sync',
                'whmcs_client_products.changed', 'whmcs_client_products', (string) $row->id,
                ['whmcs_user_id' => $userId, 'product_count' => count($products)]);
        }
        return $products;
    }

    /**
     * Strip volatile fields (timestamps, computed totals) before hashing
     * so the change-detection only fires on real edits — not just because
     * WHMCS reformatted a date.
     */
    private function canonicaliseProducts(array $products): array
    {
        $out = [];
        foreach ($products as $p) {
            $copy = $p;
            foreach (['lastupdate', 'nextduedate', 'recurringamount',
                      'firstpaymentamount', 'paymentmethod'] as $vol) {
                unset($copy[$vol]);
            }
            $out[] = $copy;
        }
        return $out;
    }

    /**
     * Field-level diff for change logging. `$skip` is fields whose value
     * is expected to drift on every save (raw payload, timestamps).
     */
    private function diff(array $before, array $after, array $skip = []): array
    {
        $diff = [];
        foreach ($after as $k => $v) {
            if (in_array($k, $skip, true)) continue;
            $b = $before[$k] ?? null;
            // Eloquent attribute casts give us Carbon / bool / etc — normalise via JSON.
            if (json_encode($b) !== json_encode($v)) {
                $diff[$k] = ['from' => $b, 'to' => $v];
            }
        }
        return $diff;
    }
}
