<?php

declare(strict_types=1);

namespace Drupal\conductor_test\Plugin\ServiceMock;

use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\http_request_mock\Attribute\ServiceMock;
use Drupal\http_request_mock\ServiceMockPluginInterface;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Intercepts any HTTP request made to app or api.conductor.com.
 */
#[ServiceMock(
  id: 'conductor_com',
  label: new TranslatableMarkup('conductor.com'),
  weight: 0,
)]

final class ApiConductorComPlugin extends PluginBase implements ServiceMockPluginInterface, ContainerFactoryPluginInterface {

  /**
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  private ModuleExtensionList $moduleExtensionList;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleExtensionList $module_extension_list) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->moduleExtensionList = $module_extension_list;

  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new self(
      $configuration, $plugin_id, $plugin_definition,
      $container->get('extension.list.module'),
    );
  }

  private const array DEFAULT_JSON_HEADER = [
    'Content-Type' => 'application/json',
    'Cache-Control' => 'no-cache',
    'Connection' => 'keep-alive',
  ];
  private const array DEFAULT_HTML_HEADER = [
    'Content-Type' => 'text/html; charset=utf-8',
    'Cache-Control' => 'no-store',
  ];

  private const array STREAMING_NDJSON_HEADER = [
    'Content-Type' => 'application/x-ndjson',
    'Cache-Control' => 'no-cache',
    'Connection' => 'keep-alive',
    'Transfer-Encoding' => 'chunked',
  ];

  /**
   * {@inheritdoc}
   */
  public function applies(RequestInterface $request, array $options): bool {
    return $request->getUri()->getHost() === 'api.conductor.com' || $request->getUri()->getHost() === 'api.conductor.com' || $request->getUri()->getHost() === 'app.conductor.com';
  }

  /**
   * {@inheritdoc}
   */
  public function getResponse(RequestInterface $request, array $options): ResponseInterface {
    $method = $request->getMethod();
    $path = $request->getUri()->getPath();

    // Simulate a 404 from the Conductor API for invalid credentials.
    parse_str($request->getUri()->getQuery(), $queryParams);
    if (($queryParams['apiKey'] ?? '') === 'invalid_api_key' && $path === '/v3/accounts') {
      return new Response(404, self::DEFAULT_JSON_HEADER, $this->getJson('get-accounts-404.json'));
    }

    // Handle streaming endpoint for content-outline/content-generation.
    if ($method === 'POST' && $path === '/v3/content-outline/content-generation') {
      return $this->getContentGenerationInStream($request);
    }
    if ($method === 'POST' && $path === '/v3/outline-expansion/content-generation') {
      return $this->getContentExpansionStream($request);
    }

    $response_map = [
      'GET' => [
        '/v3/accounts?requestUuid=1234-asdf-5678-qwer' => 'get-accounts-200-with-query-string.json',
        '/v3/accounts' => 'get-accounts-200.json',
        '/v3/accounts-500' => ['file' => 'blank.json', 'status' => 500],
        '/v3/wrong-json-header' => ['file' => 'get-accounts-200.json', 'headers' => self::DEFAULT_HTML_HEADER],
        '/v3/:accountId/rank-sources/content-guidance' => 'get-rank-sources-200.json',
        '/v3/accounts/:accountId/batch-untracked-serp/serp-explorer' => 'get-batch-untracked-serp-200.json',
        '/v3/accounts/:accountId/insights/:draftId/writing-assistant' => 'get-insights-200.json',
        '/v3/:accountId/content-guidance' => 'get-content-guidance-200.json',
        '/v3/accounts/:accountId/drafts/writing-assistant' => 'get-drafts-200.json',
        '/v3/accounts/:accountId/drafts/:draftId/writing-assistant' => 'post-get-draft-writing-assistant-200.json',
      ],
      'POST' => [
        '/v3/:accountId/content-guidance' => 'post-content-guidance-200.json',
        '/v3/accounts/:accountId/drafts/writing-assistant' => 'post-get-draft-writing-assistant-200.json',
        '/v3/accounts/:accountId/insights/:draftId/writing-assistant' => 'post-insights-200.json',
        '/v3/title-tag/content-generation' => 'post-title-tag-200.json',
        '/v3/meta-description/content-generation' => 'post-meta-description-tag-200.json',
        '/v3/:accountId/drafts/:draftId/validate-keywords/content-guidance' => 'post-validate-keywords-200.json',
      ],
      'PUT' => [
        '/v3/accounts/:accountId/drafts/:draftId/writing-assistant' => 'put-draft-writing-assistant-200.json',
      ],
      'DELETE' => [
        '/v3/accounts/:accountId/drafts/:draftId/writing-assistant' => 'delete-draft-200.json',
      ],
    ];

    return $this->resolveResponseForMethod($request, $response_map[$method] ?? []);
  }

  private function resolveResponseForMethod(RequestInterface $request, array $method_map): ResponseInterface {
    if (empty($method_map)) {
      return new Response(404, self::DEFAULT_HTML_HEADER, '<h1>Not found</h1>');
    }

    $variants_by_base = $this->buildVariantsByBase(array_keys($method_map));

    foreach ($method_map as $pattern => $entry) {
      if ($this->matchRoute($request, $pattern, $variants_by_base)) {
        [$file, $status, $headers] = $this->getResponseEntryParts($entry);
        return new Response($status, $headers, $this->getJson($file));
      }
    }

    return new Response(404, self::DEFAULT_HTML_HEADER, '<h1>Not found</h1>');
  }

  private function buildVariantsByBase(array $urls): array {
    $variants_by_base = [];
    foreach ($urls as $u) {
      $base = explode('?', $u, 2)[0];
      $has_query = str_contains($u, '?');
      $variants_by_base[$base]['with'] = ($variants_by_base[$base]['with'] ?? FALSE) || $has_query;
      $variants_by_base[$base]['without'] = ($variants_by_base[$base]['without'] ?? FALSE) || !$has_query;
    }
    return $variants_by_base;
  }

  private function getResponseEntryParts(mixed $response): array {
    $headers = self::DEFAULT_JSON_HEADER;
    if (is_array($response)) {
      $file = $response['file'];
      $status = empty($response['status']) ? $this->getHttpCodeFromFilename($file) : $response['status'];
      $headers = empty($response['headers']) ? $headers : $response['headers'];
    }
    else {
      $file = $response;
      $status = $this->getHttpCodeFromFilename($file);
    }
    return [$file, $status, $headers];
  }

  private function matchRoute(RequestInterface $request, string $pattern, array $variants_by_base): bool {
    // Decide whether to consider the query string based on whether both with- and without-
    // query variants exist in the response map for this base path.
    $url_base = explode('?', $pattern, 2)[0];
    $has_both_variants = !empty($variants_by_base[$url_base]['with']) && !empty($variants_by_base[$url_base]['without']);

    $request_path_base = $request->getUri()->getPath();
    $request_path_with_query = $this->applyQueryString($request_path_base, $request->getUri()->getQuery());

    // Default behavior: ignore query string and match on base path only.
    // Exception: if both with-query and without-query variants exist for the same base,
    // require the query to be considered (after filtering apiKey/sig).
    $candidate_pattern = $url_base;
    $candidate_path = $request_path_base;
    if ($has_both_variants) {
      $candidate_pattern = $pattern;
      $candidate_path = $request_path_with_query;
    }

    $regex = preg_quote($candidate_pattern, '#');
    $regex = preg_replace('#\\\\:([a-zA-Z_]+)#', '([^/]+)', $regex);
    $regex = '#^' . $regex . '$#';

    return (bool) preg_match($regex, $candidate_path);
  }

  private function applyQueryString(string $url, string $query_string): string {
    if (empty($query_string)) {
      return $url;
    }

    parse_str($query_string, $query_params);
    unset($query_params['apiKey'], $query_params['sig']);

    if (empty($query_params)) {
      return $url;
    }

    return $url . '?' . http_build_query($query_params);
  }

  private function getHttpCodeFromFilename(string $filename): int {
    if (preg_match('/-(\d{3})\.json$/', $filename, $matches)) {
      return (int) $matches[1];
    }
    return 200;
  }

  private function getJson(string $filename): string {
    $module_path = $this->moduleExtensionList->getPath('conductor');
    $json_file = $module_path . "/tests/fixtures/api/$filename";

    if (file_exists($json_file)) {
      $json_content = file_get_contents($json_file);
      if (is_string($json_content)) {
        return $json_content;
      }
    }

    return '{}';
  }

  /**
   * @param \Psr\Http\Message\RequestInterface $request
   *   The request.
   *
   * @return \GuzzleHttp\Psr7\Response
   */
  public function getContentGenerationInStream(RequestInterface $request): Response {
    // Create NDJSON stream data simulating AI content generation.
    $streamData = implode("\n", [
      json_encode(['chunk' => 'Explore', 'id' => NULL]),
      json_encode(['chunk' => ' the', 'id' => NULL]),
      json_encode(['chunk' => ' vibrant', 'id' => NULL]),
      json_encode(['chunk' => ' world', 'id' => NULL]),
      json_encode(['chunk' => ' of', 'id' => NULL]),
      json_encode(['chunk' => ' Philadelphia', 'id' => NULL]),
      json_encode(['chunk' => '\'s', 'id' => NULL]),
      json_encode(['chunk' => ' iconic', 'id' => NULL]),
      json_encode(['chunk' => ' sports', 'id' => NULL]),
      json_encode(['chunk' => ' masc', 'id' => NULL]),
      json_encode(['chunk' => 'ots', 'id' => NULL]),
      json_encode(['chunk' => NULL, 'id' => 'chatcmpl-CYBmCLysETD4DVwaxuqXTfGybclhB']),
      '',
    ]);

    return new Response(
      200,
      self::STREAMING_NDJSON_HEADER,
      Utils::streamFor($streamData)
    );
  }

  public function getContentExpansionStream(RequestInterface $request): Response {
    // Create NDJSON stream data simulating AI content generation.
    $streamData = implode("\n", [
      json_encode(['chunk' => 'S', 'id' => NULL]),
      json_encode(['chunk' => 'wo', 'id' => NULL]),
      json_encode(['chunk' => 'op', 'id' => NULL]),
      json_encode(['chunk' => ' has', 'id' => NULL]),
      json_encode(['chunk' => ' a', 'id' => NULL]),
      json_encode(['chunk' => ' distinct', 'id' => NULL]),
      json_encode(['chunk' => ' role', 'id' => NULL]),
      json_encode(['chunk' => ' on', 'id' => NULL]),
      json_encode(['chunk' => ' game', 'id' => NULL]),
      json_encode(['chunk' => ' day', 'id' => NULL]),
      json_encode(['chunk' => '.', 'id' => NULL]),
      json_encode(['chunk' => NULL, 'id' => 'chatcmpl-D2IjaKn5PNSfkaYffa6277CiLVnYH']),
      '',
    ]);

    return new Response(
      200,
      self::STREAMING_NDJSON_HEADER,
      Utils::streamFor($streamData)
    );
  }

}
