<?php

namespace App\Services\Whmcs;

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

/**
 * Thin wrapper around the WHMCS Admin API. Uses identifier+secret auth
 * (recommended over user/password for service-to-service).
 *
 * Methods only expose the endpoints we actually need:
 *   - GetTicket             — fetch full conversation
 *   - AddTicketNote         — post admin-only note (customer never sees)
 *   - GetClientsProducts    — find which servers a customer has
 *
 * Designed to fail closed — if WHMCS isn't configured, every method returns
 * null/false so callers can degrade gracefully.
 */
class WhmcsClient
{
    private string $url;
    private string $identifier;
    private string $secret;
    private bool $sslVerify;

    public function __construct()
    {
        // Pull from the DB-backed settings first (admin-managed via UI), fall
        // back to env. WhmcsConnection wraps both transparently.
        $cfg = app(WhmcsConnection::class)->get();
        $this->url        = rtrim((string) ($cfg['url'] ?? ''), '/');
        $this->identifier = (string) ($cfg['identifier'] ?? '');
        $this->secret     = (string) ($cfg['secret'] ?? '');
        $this->sslVerify  = (bool)   ($cfg['ssl_verify'] ?? true);
    }

    public function isConfigured(): bool
    {
        return $this->url && $this->identifier && $this->secret;
    }

    /**
     * @param array $params  WHMCS API call params
     * @param ?array{identifier:string, secret:string} $credentials  Override
     *        the default API credentials for this single call. Used by note-
     *        posting jobs to authenticate as the rotated persona's own WHMCS
     *        admin (so AddTicketNote attributes the note to the persona,
     *        not the default API user). When null, uses the connection-level
     *        identifier/secret pair.
     */
    public function call(string $action, array $params = [], ?array $credentials = null): ?array
    {
        $identifier = $credentials['identifier'] ?? $this->identifier;
        $secret     = $credentials['secret']     ?? $this->secret;
        if (! $this->url || ! $identifier || ! $secret) return null;

        try {
            // SSL verify is on by default. Operator can turn it off per
            // connection from the WHMCS Settings UI when running locally
            // without a CA bundle. Production MUST leave verify on.
            $client = Http::asForm()->timeout(15);
            if (! $this->sslVerify) {
                $client = $client->withoutVerifying();
            }
            $resp = $client->post($this->url . '/includes/api.php', array_merge([
                'identifier'   => $identifier,
                'secret'       => $secret,
                'action'       => $action,
                'responsetype' => 'json',
            ], $params));

            if (! $resp->successful()) {
                Log::warning('hsap.whmcs.http_error', ['action' => $action, 'status' => $resp->status()]);
                return null;
            }
            $body = $resp->json();
            if (($body['result'] ?? '') !== 'success') {
                Log::warning('hsap.whmcs.api_error', ['action' => $action, 'message' => $body['message'] ?? null]);
                return $body;  // still return so caller can inspect message
            }
            return $body;
        } catch (\Throwable $e) {
            Log::warning('hsap.whmcs.exception', ['action' => $action, 'err' => $e->getMessage()]);
            return null;
        }
    }

    public function getTicket(int|string $ticketId): ?array
    {
        return $this->call('GetTicket', ['ticketid' => $ticketId]);
    }

    /**
     * Internal admin note. Optionally attaches files (passed as base64-encoded
     * blobs — that's the WHMCS API requirement). Each $attachment must be
     * `['name' => 'foo.png', 'data' => '<base64-bytes>']`.
     *
     * @param ?array{identifier:string, secret:string} $credentials  Override
     *        per-call credentials so the note is attributed to a different
     *        WHMCS admin (the persona's own account). WHMCS's AddTicketNote
     *        doesn't accept an adminusername parameter — the only way to
     *        change the note author is to authenticate as that admin.
     */
    public function addTicketNote(
        int|string $ticketId,
        string $note,
        array $attachments = [],
        ?array $credentials = null,
    ): bool {
        $params = ['ticketid' => $ticketId, 'message' => $note];
        $params = array_merge($params, $this->buildAttachmentParams($attachments));
        $r = $this->call('AddTicketNote', $params, $credentials);
        return ($r['result'] ?? '') === 'success';
    }

    /**
     * Customer-facing reply. Posts as the given staff admin so the
     * customer's email shows that person as sender.
     *
     * $attachments: array of ['name' => 'foo.png', 'data' => '<base64>'].
     * WHMCS receives them via attachments[N][name|data] form fields and
     * persists them on the reply, visible in the admin panel + customer email.
     *
     * Returns the AddTicketReply result array on success, null on failure.
     * The send_enabled kill switch is enforced by callers (AutoReplyTicket
     * job), NOT here — manual staff sends from the portal must always reach
     * WHMCS regardless of the auto-reply gate.
     */
    public function addTicketReply(
        int|string $ticketId,
        string $message,
        string $adminUsername,
        array $attachments = [],
    ): ?array {
        $params = [
            'ticketid'      => $ticketId,
            'message'       => $message,
            'adminusername' => $adminUsername,
            'noemail'       => false,    // customer must receive the email
        ];
        $params = array_merge($params, $this->buildAttachmentParams($attachments));
        $r = $this->call('AddTicketReply', $params);
        return ($r['result'] ?? '') === 'success' ? $r : null;
    }

    /**
     * Translate a flat list of attachment dicts into the dotted-path form
     * fields WHMCS expects: attachments[0][name], attachments[0][data], etc.
     * Returns an empty array when there are no attachments so callers can
     * unconditionally array_merge without an extra null check.
     */
    private function buildAttachmentParams(array $attachments): array
    {
        if (empty($attachments)) return [];
        $out = [];
        foreach (array_values($attachments) as $i => $a) {
            $name = (string) ($a['name'] ?? '');
            $data = (string) ($a['data'] ?? '');
            if ($name === '' || $data === '') continue;
            $out["attachments[{$i}][name]"] = $name;
            $out["attachments[{$i}][data]"] = $data;
        }
        return $out;
    }

    /**
     * Resolve the most-recent active product/server for a client. Used to
     * decide which managed server the customer's ticket is about.
     */
    public function getClientServers(int|string $clientId): array
    {
        $r = $this->call('GetClientsProducts', ['clientid' => $clientId, 'stats' => false]);
        if (! $r) return [];

        $products = $r['products']['product'] ?? [];
        $hosts = [];
        foreach ($products as $p) {
            $domain = $p['domain']     ?? null;
            $server = $p['serverhostname'] ?? null;
            if ($domain) $hosts[] = $domain;
            if ($server) $hosts[] = $server;
        }
        return array_values(array_unique(array_filter($hosts)));
    }
}
