<?php

namespace Khatfield\Tautulli;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Collection;
use Khatfield\Tautulli\Exceptions\TautulliApiException;
use Khatfield\Tautulli\Exceptions\TautulliException;
use Khatfield\Tautulli\Exceptions\TautulliHttpException;

class TautulliClient
{
    protected Client $http;
    protected string $apiKey;
    protected string $baseEndpoint;

    public function __construct(Client $http, string $baseEndpoint, string $apiKey)
    {
        $this->http         = $http;
        $this->baseEndpoint = rtrim($baseEndpoint, '/');
        $this->apiKey       = $apiKey;
    }

    // -------------------------------------------------------------------------
    // Low-level request helpers
    // -------------------------------------------------------------------------

    /**
     * Execute a Tautulli API command and return the parsed `data` payload.
     *
     * @param  string  $cmd      The Tautulli API command, e.g. 'get_activity'.
     * @param  array   $params   Additional query-string parameters.
     * @return mixed             The value of `response.data` from the JSON body.
     *
     * @throws TautulliHttpException  On network / HTTP-level failures.
     * @throws TautulliApiException   When the Tautulli response result is not 'success'.
     * @throws TautulliException      On unexpected / malformed responses.
     */
    public function command(string $cmd, array $params = []): mixed
    {
        $query = array_merge($params, [
            'apikey' => $this->apiKey,
            'cmd'    => $cmd,
        ]);

        try {
            $response = $this->http->get($this->baseEndpoint, ['query' => $query]);
        } catch (ConnectException $e) {
            throw new TautulliHttpException(
                "Could not connect to Tautulli: {$e->getMessage()}",
                $e->getCode(),
                $e,
            );
        } catch (RequestException $e) {
            $statusCode = $e->getResponse()?->getStatusCode() ?? 0;
            throw new TautulliHttpException(
                "Tautulli HTTP error (status {$statusCode}): {$e->getMessage()}",
                $statusCode,
                $e,
            );
        } catch (GuzzleException $e) {
            throw new TautulliHttpException(
                "Tautulli request failed: {$e->getMessage()}",
                $e->getCode(),
                $e,
            );
        }

        $body = (string) $response->getBody();
        $json = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw new TautulliException('Tautulli returned a non-JSON response.');
        }

        $envelope = $json['response'] ?? null;

        if (! is_array($envelope)) {
            throw new TautulliException('Unexpected Tautulli response structure.');
        }

        if (($envelope['result'] ?? '') !== 'success') {
            throw new TautulliApiException($envelope['message'] ?? 'Unknown error');
        }

        return $envelope['data'] ?? null;
    }

    /**
     * Execute a paginated Tautulli command.
     *
     * Tautulli's table endpoints follow the DataTables convention:
     * - `start`  – zero-based row offset
     * - `length` – number of rows to return
     *
     * @param  string  $cmd
     * @param  int     $start   Zero-based row offset.
     * @param  int     $length  Rows per page.
     * @param  array   $params  Additional parameters (filters, sorting, search, etc.).
     *
     * @throws TautulliHttpException
     * @throws TautulliApiException
     * @throws TautulliException
     */
    public function paginate(
        string $cmd,
        int $start = 0,
        int $length = 25,
        array $params = [],
    ): PaginatedResponse {
        $raw = $this->command($cmd, array_merge($params, [
            'start'  => $start,
            'length' => $length,
        ]));

        if (! is_array($raw)) {
            throw new TautulliException("Command '{$cmd}' did not return a paginate-able response.");
        }

        return new PaginatedResponse(
            data:            Collection::make($raw['data'] ?? []),
            recordsTotal:    (int) ($raw['recordsTotal'] ?? 0),
            recordsFiltered: (int) ($raw['recordsFiltered'] ?? $raw['recordsTotal'] ?? 0),
            start:           $start,
            length:          $length,
        );
    }

    /**
     * Iterate through ALL pages of a paginated endpoint and return every item
     * as a single Collection.
     *
     * Use with caution on very large datasets.
     *
     * @param  string  $cmd
     * @param  array   $params  Additional parameters.
     * @param  int     $length  Page size used internally.
     *
     * @throws TautulliHttpException
     * @throws TautulliApiException
     * @throws TautulliException
     */
    public function paginateAll(string $cmd, array $params = [], int $length = 100): Collection
    {
        $items = Collection::make();
        $start = 0;

        do {
            $page  = $this->paginate($cmd, $start, $length, $params);
            $items = $items->merge($page->data);
            $start = $page->nextPageStart();
        } while ($start !== null);

        return $items;
    }

    // -------------------------------------------------------------------------
    // Activity
    // -------------------------------------------------------------------------

    /**
     * Get the current activity on the Plex Media Server.
     *
     * @param  int|null     $sessionKey  Filter by specific session key.
     * @param  string|null  $sessionId   Filter by specific session ID.
     */
    public function getActivity(?int $sessionKey = null, ?string $sessionId = null): array
    {
        $params = array_filter([
            'session_key' => $sessionKey,
            'session_id'  => $sessionId,
        ], fn ($v) => $v !== null);

        return (array) $this->command('get_activity', $params);
    }

    // -------------------------------------------------------------------------
    // History
    // -------------------------------------------------------------------------

    /**
     * Get play history (paginated).
     *
     * @param  int    $start
     * @param  int    $length
     * @param  array  $filters  Any optional get_history parameters:
     *                          user, user_id, rating_key, media_type,
     *                          order_column, order_dir, search, etc.
     */
    public function getHistory(int $start = 0, int $length = 25, array $filters = []): PaginatedResponse
    {
        return $this->paginate('get_history', $start, $length, $filters);
    }

    /**
     * Retrieve every history record as a flat Collection.
     */
    public function getAllHistory(array $filters = []): Collection
    {
        return $this->paginateAll('get_history', $filters);
    }

    // -------------------------------------------------------------------------
    // Recently Added
    // -------------------------------------------------------------------------

    /**
     * Get recently added items.
     *
     * @param  int          $count      Number of items to return.
     * @param  int          $start      Offset to start from.
     * @param  string|null  $mediaType  'movie', 'show', or 'artist'.
     * @param  string|null  $sectionId  Plex library section ID.
     */
    public function getRecentlyAdded(
        int $count = 25,
        int $start = 0,
        ?string $mediaType = null,
        ?string $sectionId = null,
    ): array {
        $params = array_filter([
            'count'      => $count,
            'start'      => $start,
            'media_type' => $mediaType,
            'section_id' => $sectionId,
        ], fn ($v) => $v !== null);

        return (array) $this->command('get_recently_added', $params);
    }

    // -------------------------------------------------------------------------
    // Libraries
    // -------------------------------------------------------------------------

    /** Get all libraries. */
    public function getLibraries(): Collection
    {
        return Collection::make((array) $this->command('get_libraries'));
    }

    /** Get the libraries table (paginated). */
    public function getLibrariesTable(int $start = 0, int $length = 25, array $params = []): PaginatedResponse
    {
        return $this->paginate('get_libraries_table', $start, $length, $params);
    }

    /** Get a single library's details. */
    public function getLibrary(string $sectionId, bool $includeLastAccessed = false): array
    {
        return (array) $this->command('get_library', [
            'section_id'            => $sectionId,
            'include_last_accessed' => $includeLastAccessed ? 'true' : 'false',
        ]);
    }

    /** Get library media info table (paginated). */
    public function getLibraryMediaInfo(
        string $sectionIdOrRatingKey,
        bool $isSectionId = true,
        int $start = 0,
        int $length = 25,
        array $params = [],
    ): PaginatedResponse {
        $key = $isSectionId ? 'section_id' : 'rating_key';

        return $this->paginate('get_library_media_info', $start, $length, array_merge(
            [$key => $sectionIdOrRatingKey],
            $params,
        ));
    }

    /** Get library watch-time statistics. */
    public function getLibraryWatchTimeStats(string $sectionId, array $params = []): Collection
    {
        return Collection::make(
            (array) $this->command('get_library_watch_time_stats', array_merge(
                ['section_id' => $sectionId],
                $params,
            ))
        );
    }

    /** Get library user statistics. */
    public function getLibraryUserStats(string $sectionId, int $grouping = 0): Collection
    {
        return Collection::make(
            (array) $this->command('get_library_user_stats', [
                'section_id' => $sectionId,
                'grouping'   => $grouping,
            ])
        );
    }

    // -------------------------------------------------------------------------
    // Users
    // -------------------------------------------------------------------------

    /** Get all users. */
    public function getUsers(): Collection
    {
        return Collection::make((array) $this->command('get_users'));
    }

    /** Get users table (paginated). */
    public function getUsersTable(int $start = 0, int $length = 25, array $params = []): PaginatedResponse
    {
        return $this->paginate('get_users_table', $start, $length, $params);
    }

    /** Get a single user's details. */
    public function getUser(string $userId, bool $includeLastSeen = false): array
    {
        return (array) $this->command('get_user', [
            'user_id'           => $userId,
            'include_last_seen' => $includeLastSeen ? 'true' : 'false',
        ]);
    }

    /** Get user IP table (paginated). */
    public function getUserIps(string $userId, int $start = 0, int $length = 25, array $params = []): PaginatedResponse
    {
        return $this->paginate('get_user_ips', $start, $length, array_merge(
            ['user_id' => $userId],
            $params,
        ));
    }

    /** Get user watch-time statistics. */
    public function getUserWatchTimeStats(string $userId, array $params = []): Collection
    {
        return Collection::make(
            (array) $this->command('get_user_watch_time_stats', array_merge(
                ['user_id' => $userId],
                $params,
            ))
        );
    }

    /** Get user player statistics. */
    public function getUserPlayerStats(string $userId, int $grouping = 0): Collection
    {
        return Collection::make(
            (array) $this->command('get_user_player_stats', [
                'user_id'  => $userId,
                'grouping' => $grouping,
            ])
        );
    }

    // -------------------------------------------------------------------------
    // Metadata
    // -------------------------------------------------------------------------

    /** Get metadata for a media item. */
    public function getMetadata(string $ratingKey): array
    {
        return (array) $this->command('get_metadata', ['rating_key' => $ratingKey]);
    }

    /** Get metadata for a media item's children. */
    public function getChildrenMetadata(string $ratingKey, string $mediaType): array
    {
        return (array) $this->command('get_children_metadata', [
            'rating_key' => $ratingKey,
            'media_type' => $mediaType,
        ]);
    }

    // -------------------------------------------------------------------------
    // Statistics & Graphs
    // -------------------------------------------------------------------------

    /** Get homepage watch statistics. */
    public function getHomeStats(array $params = []): Collection
    {
        return Collection::make((array) $this->command('get_home_stats', $params));
    }

    /** Get plays by date graph data. */
    public function getPlaysByDate(array $params = []): array
    {
        return (array) $this->command('get_plays_by_date', $params);
    }

    /** Get plays by day-of-week graph data. */
    public function getPlaysByDayOfWeek(array $params = []): array
    {
        return (array) $this->command('get_plays_by_dayofweek', $params);
    }

    /** Get plays by hour-of-day graph data. */
    public function getPlaysByHourOfDay(array $params = []): array
    {
        return (array) $this->command('get_plays_by_hourofday', $params);
    }

    /** Get plays per month graph data. */
    public function getPlaysByMonth(array $params = []): array
    {
        return (array) $this->command('get_plays_per_month', $params);
    }

    /** Get plays by top-10 platforms graph data. */
    public function getPlaysByTop10Platforms(array $params = []): array
    {
        return (array) $this->command('get_plays_by_top_10_platforms', $params);
    }

    /** Get plays by top-10 users graph data. */
    public function getPlaysByTop10Users(array $params = []): array
    {
        return (array) $this->command('get_plays_by_top_10_users', $params);
    }

    // -------------------------------------------------------------------------
    // Server / Tautulli Info
    // -------------------------------------------------------------------------

    /** Get Tautulli server info. */
    public function getTautulliInfo(): array
    {
        return (array) $this->command('get_tautulli_info');
    }

    /** Get Plex Media Server info. */
    public function getServerInfo(): array
    {
        return (array) $this->command('get_server_info');
    }

    /** Get the PMS server friendly name. */
    public function getServerFriendlyName(): string
    {
        return (string) $this->command('get_server_friendly_name');
    }

    /** Check for Plex Media Server updates. */
    public function getPmsUpdate(): array
    {
        return (array) $this->command('get_pms_update');
    }

    // -------------------------------------------------------------------------
    // Notifications
    // -------------------------------------------------------------------------

    /** Get all configured notifiers. */
    public function getNotifiers(?string $notifyAction = null): Collection
    {
        $params = array_filter(['notify_action' => $notifyAction], fn ($v) => $v !== null);

        return Collection::make((array) $this->command('get_notifiers', $params));
    }

    /** Get notification log (paginated). */
    public function getNotificationLog(int $start = 0, int $length = 25, array $params = []): PaginatedResponse
    {
        return $this->paginate('get_notification_log', $start, $length, $params);
    }

    /**
     * Send a notification via a configured notifier.
     *
     * @param  int     $notifierId
     * @param  string  $subject
     * @param  string  $body
     * @param  array   $extra       Optional: headers, script_args.
     */
    public function notify(int $notifierId, string $subject, string $body, array $extra = []): array
    {
        return (array) $this->command('notify', array_merge([
            'notifier_id' => $notifierId,
            'subject'     => $subject,
            'body'        => $body,
        ], $extra));
    }

    // -------------------------------------------------------------------------
    // Utility
    // -------------------------------------------------------------------------

    /** Get geolocation info for an IP address. */
    public function getGeoipLookup(string $ipAddress): array
    {
        return (array) $this->command('get_geoip_lookup', ['ip_address' => $ipAddress]);
    }

    /** Get Tautulli logs (paginated). */
    public function getLogs(int $start = 0, int $end = 25, array $params = []): array
    {
        return (array) $this->command('get_logs', array_merge([
            'start' => $start,
            'end'   => $end,
        ], $params));
    }

    /** Get stream details from history or the current stream. */
    public function getStreamData(int $rowIdOrSessionKey, bool $isSessionKey = false): array
    {
        $key = $isSessionKey ? 'session_key' : 'row_id';

        return (array) $this->command('get_stream_data', [$key => $rowIdOrSessionKey]);
    }

    /** Get date and time formats configured in Tautulli. */
    public function getDateFormats(): array
    {
        return (array) $this->command('get_date_formats');
    }

    /** Refresh the libraries list. */
    public function refreshLibrariesList(): void
    {
        $this->command('refresh_libraries_list');
    }

    /** Refresh the users list. */
    public function refreshUsersList(): void
    {
        $this->command('refresh_users_list');
    }

    /** Create a manual config backup. */
    public function backupConfig(): void
    {
        $this->command('backup_config');
    }

    /** Create a manual database backup. */
    public function backupDb(): void
    {
        $this->command('backup_db');
    }
}
