<?php

namespace IPS\forumfortress\Api;

use IPS\Application;
use IPS\Db;
use IPS\Http\Url;
use IPS\Log;
use IPS\Request;
use IPS\Settings;
use Throwable;
use function array_map;
use function array_unique;
use function array_values;
use function explode;
use function gmdate;
use function in_array;
use function is_array;
use function is_int;
use function json_decode;
use function json_encode;
use function ksort;
use function max;
use function mb_strpos;
use function microtime;
use function min;
use function parse_url;
use function preg_match_all;
use function round;
use function rtrim;
use function sha1;
use function strtolower;
use function strtoupper;
use function time;
use function trim;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

class Client
{
	public const PLATFORM = 'invision';
	public const PLUGIN_VERSION = '1.0.0-beta1';

	public static function isEnabled(): bool
	{
		return (bool) Settings::i()->ff_enabled;
	}

	public static function baseUrl(): string
	{
		return rtrim( trim( Settings::i()->ff_api_base_url ), '/' );
	}

	protected static function normaliseBaseUrl( string $value ): string
	{
		return rtrim( trim( $value ), '/' );
	}

	public static function forumDomain(): string
	{
		$base = Settings::i()->base_url;
		$host = parse_url( (string) $base, PHP_URL_HOST );

		if ( $host )
		{
			return strtolower( (string) $host );
		}

		$fallback = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';

		return strtolower( (string) $fallback );
	}

	public static function bootstrapIfNeeded(): ?array
	{
		if ( !static::isEnabled() or trim( Settings::i()->ff_api_key ) !== '' )
		{
			return NULL;
		}

		$response = static::request(
			'POST',
			'/v1/site/bootstrap',
			[
				'domain' => static::forumDomain(),
				'platform' => static::PLATFORM,
				'platform_version' => Application::load( 'core' )->version,
				'plugin_version' => static::PLUGIN_VERSION,
				'api_key' => NULL,
			]
		);

		return $response;
	}

	public static function ensureIdentity(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$key = trim( (string) Settings::i()->ff_api_key );
		$siteId = trim( (string) Settings::i()->ff_site_id );

		if ( $key === '' )
		{
			return static::bootstrapIfNeeded();
		}

		if ( $siteId === '' )
		{
			return static::siteStatus();
		}

		static::refreshEndpointCatalogAndHealth();

		return NULL;
	}

	public static function checkRegisterFromRequest(): ?array
	{
		static::bootstrapIfNeeded();

		$username = (string) Request::i()->username;
		$email = (string) Request::i()->email_address;
		$payload = [
			'ip' => Request::i()->ipAddress(),
			'username' => $username,
			'email' => $email,
			'email_domain' => static::emailDomain( $email ),
			'user_agent' => (string) ( Request::i()->userAgent() ?? '' ),
			'account_age_seconds' => 0,
			'post_count' => 0,
			'content' => NULL,
			'links' => [],
		];

		return static::checkRegisterPayload( $payload );
	}

	public static function checkRegisterPayload( array $payload ): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$prepared = static::preparePayload( $payload );
		$timeout = max( 1, min( 2, (int) Settings::i()->ff_timeout ) );

		return static::request( 'POST', '/v1/check/register', $prepared, $timeout );
	}

	public static function reportRegister( \IPS\Member $member ): void
	{
		if ( !static::isEnabled() or !$member->member_id )
		{
			return;
		}

		static::bootstrapIfNeeded();

		$email = trim( (string) $member->email );
		$payload = [
			'forum_id' => NULL,
			'username' => (string) $member->name,
			'email_domain' => static::emailDomain( $email ),
			'ip' => $member->ip_address ?: Request::i()->ipAddress(),
			'links' => [],
			'payload' => [
				'source' => 'invision_registration',
				'member_id' => $member->member_id,
			],
		];

		static::request( 'POST', '/v1/report/register', static::preparePayload( $payload ) );
	}

	public static function hourlySync(): void
	{
		if ( !static::isEnabled() )
		{
			return;
		}

		try
		{
			static::bootstrapIfNeeded();
		}
		catch ( Throwable $e )
		{
		}

		try
		{
			static::refreshEndpointCatalogAndHealth();
		}
		catch ( Throwable $e )
		{
		}

		try
		{
			static::request( 'GET', '/v1/plugin-release', [
				'platform' => static::PLATFORM,
				'current_version' => static::PLUGIN_VERSION,
			] );
		}
		catch ( Throwable $e )
		{
		}

		try
		{
			static::sitePing();
		}
		catch ( Throwable $e )
		{
		}

		try
		{
			static::syncModerationQueue();
		}
		catch ( Throwable $e )
		{
		}
	}

	public static function sitePing(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$siteId = trim( Settings::i()->ff_site_id );
		$apiKey = trim( Settings::i()->ff_api_key );
		if ( $siteId === '' || $apiKey === '' )
		{
			return NULL;
		}

		return static::request( 'POST', '/v1/site/ping', [
			'api_key' => $apiKey,
			'site_id' => $siteId,
			'domain' => static::forumDomain(),
			'platform' => static::PLATFORM,
			'platform_version' => Application::load( 'core' )->version,
			'plugin_version' => static::PLUGIN_VERSION,
		] );
	}

	public static function checkContent( string $endpoint, array $payload ): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		static::bootstrapIfNeeded();

		return static::request( 'POST', '/v1/check/' . $endpoint, static::preparePayload( $payload ) );
	}

	public static function health(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		return static::request( 'GET', '/health', [] );
	}

	public static function capabilities(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		return static::request( 'GET', '/v1/capabilities', [] );
	}

	public static function siteStatus(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$key = trim( Settings::i()->ff_api_key );
		if ( $key === '' )
		{
			return NULL;
		}

		return static::request( 'GET', '/v1/site/status', [
			'api_key' => $key,
			'domain' => static::forumDomain(),
		] );
	}

	public static function forumStats(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$key = trim( Settings::i()->ff_api_key );
		if ( $key === '' )
		{
			return NULL;
		}

		return static::request( 'GET', '/v1/forum/stats', [
			'api_key' => $key,
			'domain' => static::forumDomain(),
		] );
	}

	public static function pluginRelease(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		return static::request( 'GET', '/v1/plugin-release', [
			'platform' => static::PLATFORM,
			'current_version' => static::PLUGIN_VERSION,
		] );
	}

	public static function registerSite( string $email ): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$siteId = trim( Settings::i()->ff_site_id );
		if ( $siteId === '' )
		{
			static::bootstrapIfNeeded();
			$siteId = trim( Settings::i()->ff_site_id );
		}

		if ( $siteId === '' || $email === '' )
		{
			return NULL;
		}

		return static::request( 'POST', '/v1/site/register', [
			'domain' => static::forumDomain(),
			'email' => trim( $email ),
			'site_id' => $siteId,
		] );
	}

	public static function portalLaunch(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$siteId = trim( Settings::i()->ff_site_id );
		$apiKey = trim( Settings::i()->ff_api_key );
		if ( $siteId === '' || $apiKey === '' )
		{
			static::bootstrapIfNeeded();
			$siteId = trim( Settings::i()->ff_site_id );
			$apiKey = trim( Settings::i()->ff_api_key );
		}

		if ( $siteId === '' || $apiKey === '' )
		{
			return NULL;
		}

		return static::request( 'POST', '/v1/site/portal', [
			'api_key' => $apiKey,
			'site_id' => $siteId,
			'domain' => static::forumDomain(),
			'platform' => static::PLATFORM,
			'platform_version' => Application::load( 'core' )->version,
			'plugin_version' => static::PLUGIN_VERSION,
		] );
	}

	public static function syncModerationQueue(): ?array
	{
		if ( !static::isEnabled() )
		{
			return NULL;
		}

		$siteId = trim( Settings::i()->ff_site_id );
		$apiKey = trim( Settings::i()->ff_api_key );
		if ( $siteId === '' || $apiKey === '' )
		{
			return NULL;
		}

		$rows = iterator_to_array(
			Db::i()->select(
				'*',
				'core_approval_queue',
				NULL,
				'approval_id DESC',
				200
			)
		);

		$items = [];
		foreach ( $rows as $row )
		{
			$class = (string) ( $row['approval_content_class'] ?? '' );
			$type = static::mapApprovalClassToType( $class );
			if ( $type === NULL )
			{
				continue;
			}

			$heldData = [];
			$decoded = json_decode( (string) ( $row['approval_held_data'] ?? '' ), TRUE );
			if ( is_array( $decoded ) )
			{
				$heldData = $decoded;
			}

			$items[] = [
				'remote_content_type' => $type,
				'remote_content_id' => (int) ( $row['approval_content_id'] ?? 0 ),
				'reason' => (string) ( $row['approval_held_reason'] ?? '' ),
				'excerpt' => (string) ( $heldData['content'] ?? '' ),
				'username' => (string) ( $heldData['name'] ?? '' ),
				'content_hash' => sha1( (string) ( $heldData['content'] ?? '' ) ),
				'payload' => [ 'approval_id' => (int) ( $row['approval_id'] ?? 0 ) ],
			];
		}

		return static::request( 'POST', '/v1/moderation-queue/sync', [
			'api_key' => $apiKey,
			'site_id' => $siteId,
			'domain' => static::forumDomain(),
			'platform' => static::PLATFORM,
			'platform_version' => Application::load( 'core' )->version,
			'plugin_version' => static::PLUGIN_VERSION,
			'items' => $items,
		] );
	}

	protected static function preparePayload( array $payload ): array
	{
		$key = trim( Settings::i()->ff_api_key );
		$out = array_merge(
			[
				'domain' => static::forumDomain(),
				'platform' => static::PLATFORM,
			],
			$payload
		);

		if ( $key !== '' )
		{
			$out['api_key'] = $key;
		}

		return $out;
	}

	protected static function persistIdentity( array $response ): void
	{
		$updates = [];

		if ( !empty( $response['api_key'] ) && $response['api_key'] !== Settings::i()->ff_api_key )
		{
			$updates['ff_api_key'] = (string) $response['api_key'];
		}

		if ( !empty( $response['site_id'] ) && $response['site_id'] !== Settings::i()->ff_site_id )
		{
			$updates['ff_site_id'] = (string) $response['site_id'];
		}

		if ( $updates )
		{
			Settings::i()->changeValues( $updates );
		}
	}

	protected static function loadEndpointState(): array
	{
		$raw = trim( (string) Settings::i()->ff_endpoint_state );
		if ( $raw === '' )
		{
			return [];
		}

		$data = json_decode( $raw, TRUE );

		return is_array( $data ) ? $data : [];
	}

	protected static function saveEndpointState( array $state ): void
	{
		ksort( $state );
		$encoded = json_encode( $state, JSON_UNESCAPED_SLASHES );
		if ( !is_string( $encoded ) )
		{
			return;
		}

		if ( (string) Settings::i()->ff_endpoint_state === $encoded )
		{
			return;
		}

		Settings::i()->changeValues( [ 'ff_endpoint_state' => $encoded ] );
	}

	public static function endpointStateSummary(): array
	{
		$state = static::loadEndpointState();

		return [
			'preferred' => (string) ( $state['preferred'] ?? static::baseUrl() ),
			'endpoints_count' => is_array( $state['endpoints'] ?? NULL ) ? count( $state['endpoints'] ) : 1,
			'last_health_at' => (int) ( $state['last_health_at'] ?? 0 ),
		];
	}

	public static function refreshEndpointCatalogAndHealth(): void
	{
		if ( !static::isEnabled() )
		{
			return;
		}

		$manual = static::normaliseBaseUrl( static::baseUrl() );
		if ( $manual === '' )
		{
			return;
		}

		$state = static::loadEndpointState();
		$now = time();
		$dayKey = gmdate( 'Y-m-d', $now );

		if ( empty( $state['catalog_fetched_at'] ) || ( $now - (int) $state['catalog_fetched_at'] ) > 3600 )
		{
			$res = static::rawRequest( 'GET', $manual, '/v1/node-endpoints', [], 5 );
			$urls = [ $manual => TRUE ];
			if ( ( $res['status'] ?? 0 ) >= 200 && ( $res['status'] ?? 0 ) < 300 && is_array( $res['data'] ?? NULL ) )
			{
				$endpoints = $res['data']['endpoints'] ?? NULL;
				if ( is_array( $endpoints ) )
				{
					foreach ( $endpoints as $row )
					{
						$url = static::normaliseBaseUrl( (string) ( $row['url'] ?? '' ) );
						if ( $url !== '' )
						{
							$urls[ $url ] = TRUE;
						}
					}
				}
			}
			$state['catalog_fetched_at'] = $now;
			$state['endpoints'] = array_values( array_keys( $urls ) );
		}

		$list = $state['endpoints'] ?? [];
		if ( !is_array( $list ) || !$list )
		{
			$state['endpoints'] = [ $manual ];
		}

		$lastHealth = (int) ( $state['last_health_at'] ?? 0 );
		$healthDay = (string) ( $state['health_day'] ?? '' );
		if ( $healthDay !== $dayKey || ( $now - $lastHealth ) > 86400 )
		{
			$latencies = [];
			$candidates = is_array( $state['endpoints'] ) ? $state['endpoints'] : [];
			$candidates[] = $manual;
			$candidates = array_values( array_unique( array_map( static fn( $u ) => static::normaliseBaseUrl( (string) $u ), $candidates ) ) );
			$candidates = array_values( array_filter( $candidates, static fn( $u ) => $u !== '' ) );
			foreach ( $candidates as $base )
			{
				$t0 = microtime( TRUE );
				$hr = static::rawRequest( 'GET', $base, '/health', [], 3 );
				$ms = NULL;
				if ( ( $hr['status'] ?? 0 ) >= 200 && ( $hr['status'] ?? 0 ) < 300 )
				{
					$ms = (int) round( ( microtime( TRUE ) - $t0 ) * 1000 );
				}
				$latencies[ $base ] = $ms;
			}
			$best = $manual;
			$bestMs = 999999;
			foreach ( $latencies as $b => $ms )
			{
				if ( is_int( $ms ) && $ms >= 0 && $ms < $bestMs )
				{
					$bestMs = $ms;
					$best = $b;
				}
			}
			$state['last_health_at'] = $now;
			$state['health_day'] = $dayKey;
			$state['health_ms'] = $latencies;
			$state['preferred'] = $best;
		}

		static::saveEndpointState( $state );
	}

	protected static function getOrderedBasesForRequests(): array
	{
		$manual = static::normaliseBaseUrl( static::baseUrl() );
		if ( $manual === '' )
		{
			return [];
		}
		$state = static::loadEndpointState();
		$endpoints = $state['endpoints'] ?? NULL;
		if ( !is_array( $endpoints ) || !$endpoints )
		{
			$endpoints = [ $manual ];
		}
		$endpoints = array_values( array_unique( array_map( static fn( $u ) => static::normaliseBaseUrl( (string) $u ), $endpoints ) ) );
		$endpoints = array_values( array_filter( $endpoints, static fn( $u ) => $u !== '' ) );
		$preferred = static::normaliseBaseUrl( (string) ( $state['preferred'] ?? $manual ) );
		if ( $preferred === '' || !in_array( $preferred, $endpoints, TRUE ) )
		{
			$preferred = $endpoints[0];
		}
		$out = [ $preferred ];
		foreach ( $endpoints as $base )
		{
			if ( !in_array( $base, $out, TRUE ) )
			{
				$out[] = $base;
			}
		}
		if ( !in_array( $manual, $out, TRUE ) )
		{
			$out[] = $manual;
		}

		return $out;
	}

	protected static function markPreferredBaseAfterFailover( string $baseUrl ): void
	{
		$baseUrl = static::normaliseBaseUrl( $baseUrl );
		if ( $baseUrl === '' )
		{
			return;
		}
		$state = static::loadEndpointState();
		$state['preferred'] = $baseUrl;
		$state['failover_at'] = time();
		static::saveEndpointState( $state );
	}

	protected static function rawRequest( string $method, string $baseUrl, string $path, array $body, int $timeout ): array
	{
		$out = [ 'status' => 0, 'body' => '', 'data' => NULL, 'error' => NULL ];
		$baseUrl = static::normaliseBaseUrl( $baseUrl );
		if ( $baseUrl === '' )
		{
			$out['error'] = 'empty_base';
			return $out;
		}

		try
		{
			$url = Url::external( $baseUrl . $path );

			if ( strtoupper( $method ) === 'GET' && $body )
			{
				$url = $url->setQueryString( $body );
			}

			$curl = $url->request( $timeout );
			$headers = [ 'Accept' => 'application/json' ];

			if ( strtoupper( $method ) !== 'GET' )
			{
				$headers['Content-Type'] = 'application/json';
			}

			$curl->setHeaders( $headers );

			$resp = strtoupper( $method ) === 'GET'
				? $curl->get()
				: $curl->post( json_encode( $body ) );
			$out['status'] = (int) $resp->httpResponseCode;
			$out['body'] = (string) $resp;
			if ( $out['body'] !== '' )
			{
				$decoded = json_decode( $out['body'], TRUE );
				$out['data'] = is_array( $decoded ) ? $decoded : NULL;
			}
		}
		catch ( Throwable $e )
		{
			$out['error'] = $e->getMessage();
		}

		return $out;
	}

	public static function request( string $method, string $path, array $body, ?int $timeoutOverride = NULL ): ?array
	{
		$bases = static::getOrderedBasesForRequests();
		if ( !$bases )
		{
			return NULL;
		}
		$timeout = $timeoutOverride ?? max( 1, (int) Settings::i()->ff_timeout );

		foreach ( $bases as $index => $base )
		{
			$res = static::rawRequest( $method, $base, $path, $body, $timeout );
			$status = (int) ( $res['status'] ?? 0 );
			$data = $res['data'] ?? NULL;
			if ( $status >= 200 && $status < 300 && is_array( $data ) )
			{
				static::persistIdentity( $data );
				if ( $index > 0 )
				{
					static::markPreferredBaseAfterFailover( $base );
				}
				return $data;
			}

			$canFailover = $status === 0 || $status === 520 || ( $status >= 502 && $status <= 504 );
			if ( !$canFailover )
			{
				if ( Settings::i()->ff_debug_log )
				{
					Log::debug( [ 'ff_path' => $path, 'status' => $status, 'base' => $base, 'body' => (string) ( $res['body'] ?? '' ) ], 'forumfortress' );
				}
				return NULL;
			}
		}

		return NULL;
	}

	public static function emailDomain( string $email ): ?string
	{
		if ( $email === '' || mb_strpos( $email, '@' ) === FALSE )
		{
			return NULL;
		}

		$parts = explode( '@', $email, 2 );

		return isset( $parts[1] ) ? strtolower( trim( $parts[1] ) ) : NULL;
	}

	public static function extractLinks( string $text ): array
	{
		if ( $text === '' )
		{
			return [];
		}

		preg_match_all( '#https?://[^\s<>"\']+#i', $text, $matches );
		$links = is_array( $matches[0] ?? NULL ) ? $matches[0] : [];

		return array_values( array_unique( array_map( 'strval', $links ) ) );
	}

	protected static function mapApprovalClassToType( string $class ): ?string
	{
		if ( $class === 'IPS\\forums\\Topic' )
		{
			return 'thread';
		}
		if ( $class === 'IPS\\forums\\Topic\\Post' )
		{
			return 'post';
		}

		return NULL;
	}
}
