<?php

declare(strict_types=1);

namespace Drupal\acquia_dam_test\HttpClientMiddleware;

use Drupal\Component\Serialization\Json;
use Drupal\Core\State\StateInterface;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;

/**
 * Intercepts API requests to the DAM to return mocked data.
 */
final class MockedResponseMiddleware {

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private $logger;

  /**
   * The state storage.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * Constructs a new MockedResponseMiddleware object.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state storage.
   */
  public function __construct(LoggerInterface $logger, StateInterface $state) {
    $this->logger = $logger;
    $this->state = $state;
  }

  /**
   * Middleware callable to handle mocked responses.
   */
  public function __invoke(): callable {
    return function (callable $handler): callable {
      return function (RequestInterface $request, array $options) use ($handler): PromiseInterface {
        $path = $request->getUri()->getPath();
        if ($path == '/api/rest/oauth/token') {
          $auth_content = json_decode($request->getBody()->getContents());
          if ($auth_content->authorization_code == 'server_error') {
            return new FulfilledPromise(new Response(502));
          }
          elseif ($auth_content->authorization_code == 'client_error') {
            return new FulfilledPromise(new Response(403));
          }
        }
        if ($request->getUri()->getHost() === 'test.widencollective.disconnect') {
          return new FulfilledPromise(new Response(200));
        }
        if ($request->getUri()->getHost() === 'test.widencollective.connect') {
          return new FulfilledPromise(new Response(200));
        }
        if ($request->getUri()->getHost() === 'test.widencollective.com' ||
          $request->getUri()->getHost() === 'test2.widencollective.com') {
          $path = $request->getUri()->getPath();
          if ($path === '/api/rest/oauth/token') {
            return new FulfilledPromise(
              new Response(
                200,
                ['Content-Type' => 'application/json'],
                Json::encode([
                  'access_token' => 'ABC12',
                ])
              ));
          }
          if ($path === '/api/rest/metadata/types') {
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/metadata/metadata-fields-v1.json");
            assert($response !== FALSE);
            return new FulfilledPromise(new Response(200, [], $response));
          }
        }
        if ($request->getUri()->getHost() === 'api.widencollective.com') {
          $path = $request->getUri()->getPath();
          if ($path === '/v2/test') {
            if ($request->getHeader('Authorization')[0] === 'Bearer wat_laser_b1d3c61e03c65d0650550f35a330249e') {
              return new FulfilledPromise(new Response(200));
            }
            return new FulfilledPromise(new Response(401));
          }
          if ($path === '/v2/oauth/access-token') {
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/access-token-using-refresh-token.json");
            return new FulfilledPromise(new Response(200, [], $response));
          }
        }
        if ($request->getUri()->getHost() === 'embed.widencdn.net' ||
          $request->getUri()->getHost() === 'test.widen.net') {
          preg_match('/\/([0-9a-z]{10})\//', $path, $asset_external_id);
          $image = file_get_contents(__DIR__ . '/../../../../fixtures/remote_thumbnails/' . $asset_external_id[1] . '.png');
          return new FulfilledPromise(
            new Response(200, [
              'Content-Type' => 'image/png',
              'Content-Length' => strlen($image),
            ], $image)
          );
        }
        if ($request->getUri()->getHost() === 'previews.us-east-1.widencdn.net') {
          $image = file_get_contents(__DIR__ . "/../../../../fixtures/preview-thumbnail.png");
          return new FulfilledPromise(
            new Response(200, [
              'Content-Type' => 'image/png',
              'Content-Length' => strlen($image),
            ], $image)
          );
        }

        // Requesting a given asset by its download URL.
        if ($request->getUri()->getHost() === 'orders-bb.us-east-1.widencdn.net') {
          $file = file_get_contents(__DIR__ . '/../../../../fixtures/wheel-illustration.ai');
          return new FulfilledPromise(
            new Response(200, [
              'Content-Type' => 'application/octet-stream',
              'Content-Length' => strlen($file),
            ], $file)
          );
        }

        if ($request->getUri()->getHost() === 'laser.widen.net' && str_contains($request->getUri()->getPath(), '/content')) {
          $parts = explode('/', $request->getUri()->getPath());
          $format = $parts[3];
          if ($format === 'web') {
            $image = file_get_contents(__DIR__ . "/../../../../fixtures/preview-thumbnail.png");
            $header = [
              'Content-Type' => 'image/png',
              'Content-Length' => strlen($image),
            ];
            return new FulfilledPromise(new Response(200, $header, $image));
          }
        }

        $path = $request->getUri()->getPath();

        if ($path === '/collective.ping') {
          return new FulfilledPromise(new Response(200));
        }

        if (!str_contains($request->getUri()->getHost(), 'widencollective.com')) {
          return $handler($request, $options);
        }

        $query = [];
        parse_str(urldecode($request->getUri()->getQuery()), $query);
        $search_mapping = [
          'pdf' => 'Explorer',
          'video' => 'SD-Social',
          'SpinSet' => 'eudaimonia_spin',
          'image' => '422-lake-shore',
          'office' => 'Best',
          'audio' => 'Linus-linux',
          'generic' => 'info',
        ];
        $search_query = $query['query'] ?? '';

        // Handle trigger for a 4xx response.
        if (strpos($search_query, '4xx_error') !== FALSE) {
          return new FulfilledPromise(new Response(400));
        }

        // Handle trigger for a 5xx response.
        if (strpos($search_query, '5xx_error') !== FALSE) {
          return new FulfilledPromise(new Response(500));
        }
        if ($path === '/v2/assets/search') {
          // Scroll search test mode.
          // Activated when 'acquia_dam_test.scroll_search_mode' state is set AND
          // the request carries scroll=true (always set by scrollSearch() internally).
          $scroll_mode = $this->state->get('acquia_dam_test.scroll_search_mode');
          if ($scroll_mode !== NULL && ($query['scroll'] ?? '') === 'true') {
            // Reset the continuation counter each time a fresh scroll session
            // starts so test methods do not bleed state into one another.
            $this->state->set('acquia_dam_test.scroll_continuation_call_count', 0);
            if ($scroll_mode === 'empty') {
              // Simulate a zero-result first page: scroll terminates immediately.
              $response = file_get_contents(__DIR__ . '/../../../../fixtures/search/scroll/search_complete.json');
              assert($response !== FALSE);
              return new FulfilledPromise(new Response(200, [], $response));
            }
            // 'success' and 'error' modes both serve the real first-page fixture.
            $response = file_get_contents(__DIR__ . '/../../../../fixtures/search/scroll/search.json');
            assert($response !== FALSE);
            return new FulfilledPromise(new Response(200, [], $response));
          }

          // When scroll search test mode is not active.
          if (!$scroll_mode && str_contains($search_query, 'lastEditDate')) {
            // Allow tests to override the fixture via state so that different
            // cron scenarios (released, unreleased, multi-asset, etc.) can be
            // exercised without changing the global default fixture.
            $fixture_file = $this->state->get(
              'acquia_dam_test.last_edited_search_fixture',
              'lastEditedSearch.json'
            );
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/$fixture_file");
            assert($response !== FALSE, "Fixture file '$fixture_file' not found.");
            return new FulfilledPromise(new Response(200, [], $response));
          }
          // Media library search.
          if ((preg_match("/ft:\((\w*)\)/i", $search_query, $matches) === 1
            || preg_match("/ff:\((\w*)\)/i", $search_query, $matches) === 1)) {
            $file_type = strtok($search_query, ' ');
            if ($file_type === 'ft:(' . $matches[1] . ')' || $file_type === 'ff:(' . $matches[1] . ')') {
              $fixture_file = 'getSearch.json';
              if ($query['include_deleted'] == 'true') {
                $fixture_file = 'getSearchIncludeDeleted.json';
              }
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/$matches[1]/$fixture_file");
            } elseif (strpos($search_query, $search_mapping[$matches[1]]) !== FALSE) {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/$matches[1]/getSearchFile.json");
            } elseif (preg_match('/asset_type:\(\{(\w*)\}\)/i', $search_query, $results) === 1) {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/search/assettype-$results[1].json");
            } elseif (preg_match('/keywords:\((\w*)\)/i', $search_query, $results) === 1) {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/search/keywords-$results[1].json");
            } else {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/$matches[1]/getSearchEmpty.json");
            }
            assert($response !== FALSE);
            return new FulfilledPromise(new Response(200, [], $response));
          }
          elseif (preg_match("/^cat:\(\{?(\w*)\}?\)/i", $search_query, $matches) === 1) {
            if (preg_match("/^ft:\(\{?(\w*)\}?\)/i", $search_query, $matches) === 1) {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/search/categorySearchEmpty.json");
            }
            else {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/search/categorySearch.json");
            }
            assert($response !== FALSE);
            return new FulfilledPromise(new Response(200, [], $response));
          }
          elseif (preg_match("/^ag:\(\{?(\w*)\}?\)/i", $search_query, $matches) === 1) {
            if (preg_match("/^ft:\(\{?(\w*)\}?\)/i", $search_query, $matches) === 1) {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/search/assetGroupSearchEmpty.json");
            }
            else {
              $response = file_get_contents(__DIR__ . "/../../../../fixtures/search/assetGroupSearch.json");
            }
            assert($response !== FALSE);
            return new FulfilledPromise(new Response(200, [], $response));
          }
        }
        // Scroll search continuation endpoint.
        // Must appear BEFORE the /v2/assets/ catch-all, which would otherwise
        // match /v2/assets/search/scroll and produce a RuntimeException.
        if ($path === '/v2/assets/search/scroll') {
          $scroll_mode = $this->state->get('acquia_dam_test.scroll_search_mode');
          if ($scroll_mode !== NULL) {
            if ($scroll_mode === 'error') {
              // Simulate an expired or invalid scroll_id (JSON-body error, HTTP 200).
              return new FulfilledPromise(new Response(200, [], Json::encode([
                'error' => TRUE,
                'message' => 'Scroll ID expired.',
              ])));
            }
            // 'success' mode: route by call count.
            // count=0 → page 2 (search_next_page.json), then count becomes 1.
            // count≥1 → terminator (search_complete.json with empty items).
            $call_count = (int) $this->state->get('acquia_dam_test.scroll_continuation_call_count', 0);
            if ($call_count === 0) {
              $this->state->set('acquia_dam_test.scroll_continuation_call_count', 1);
              $response = file_get_contents(__DIR__ . '/../../../../fixtures/search/scroll/search_next_page.json');
              assert($response !== FALSE);
              return new FulfilledPromise(new Response(200, [], $response));
            }
            // Second continuation call: empty items → scrollSearch() breaks loop.
            $response = file_get_contents(__DIR__ . '/../../../../fixtures/search/scroll/search_complete.json');
            assert($response !== FALSE);
            return new FulfilledPromise(new Response(200, [], $response));
          }
        }

        // Asset data query in API v2.
        if (strpos($path, '/v2/assets/') === 0) {
          $asset_id = str_replace('/v2/assets/', '', $path);

          // Allow tests to override the fixture for a specific asset via state,
          // e.g. to simulate a RELEASED → UNRELEASED status transition mid-test.
          $asset_fixture_key = 'acquia_dam_test.asset_fixture_' . $asset_id;
          if ($fixture_file = $this->state->get($asset_fixture_key)) {
            $fixture_path = __DIR__ . "/../../../../fixtures/$fixture_file";
            if (file_exists($fixture_path)) {
              $response = file_get_contents($fixture_path);
              assert($response !== FALSE);
              $data = Json::decode($response);
              return new FulfilledPromise(new Response(200, [], Json::encode($data)));
            }
          }

          if (file_exists(__DIR__ . "/../../../../fixtures/$asset_id.json")) {
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/$asset_id.json");
            // Adjust the mocked response so that a valid thumbnail URL is
            // available. Thumbnail URLs are temporary and generated when the
            // asset is fetched from the API. Using a mock response prevents
            // fetching that thumbnail.
            $data = Json::decode($response);

            $status_code = $asset_id === 'c2bbed58-427f-43f7-91d8-c380307dac67' ? 404 : 200;
            return new FulfilledPromise(new Response($status_code, [], Json::encode($data)));
          }
        }
        // Asset version list query in API v2.
        if (preg_match('/\/v2\/assets\/([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\/versions/i', $path, $matches) === 1) {
          $response = file_get_contents(__DIR__ . "/../../../../fixtures/versions/$matches[1].json");
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }
        if ($path === '/api/rest/integrationlink') {
          $auth_content = json_decode($request->getBody()->getContents());
          // If register for node we need a different integration link id.
          if (str_contains($auth_content->url, 'node')) {
            $response = file_get_contents(__DIR__ . '/../../../../fixtures/integration_links/postIntegrationLinkNode.json');
            assert($response !== FALSE);

            $data = Json::decode($response);
            return new FulfilledPromise(new Response(200, [], Json::encode($data)));
          }
          $response = file_get_contents(__DIR__ . '/../../../../fixtures/integration_links/postIntegrationLink.json');
          assert($response !== FALSE);
          $data = Json::decode($response);
          return new FulfilledPromise(new Response(200, [], Json::encode($data)));
        }

        // Integration link delete request.
        if ($path === '/api/rest/integrationlink/ae93661e-bc39-4997-8f7d-c957aaade231') {
          return new FulfilledPromise(new Response(200));
        }

        if ($path === '/api/rest/integrationlink/ae93661e-bc39-4997-8f7d-c957aaade238') {
          return new FulfilledPromise(new Response(204));
        }

        if ($path == '/api/rest/oauth/token') {
          $auth_content = json_decode($request->getBody()->getContents());
          if ($auth_content->authorization_code == 'server_error') {
            return new FulfilledPromise(new Response(502));
          }
          elseif ($auth_content->authorization_code == 'client_error') {
            return new FulfilledPromise(new Response(403));
          }
          elseif ($auth_content->authorization_code == 'pass') {
            $response = file_get_contents(__DIR__ . '/../../../../fixtures/authBody.json');
            assert($response !== FALSE);
            $data = Json::decode($response);
            return new FulfilledPromise(new Response(200, [], Json::encode($data)));
          }
        }

        if ($path == '/api/rest/oauth/logout') {
          return new FulfilledPromise(new Response(200));
        }

        if ($path === '/v2/metadata/fields/viewable') {
          if ($query['field_types'] === 'text,text_short,text_long') {
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/viewableTextTypes.json");
          }
          elseif (str_contains($query['field_types'], 'selection_list')) {
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/viewableSelectList.json");
          }
          elseif ($query['field_types'] === 'all') {
            $response = file_get_contents(__DIR__ . "/../../../../fixtures/viewableTextTypes.json");
          }
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }

        if ($path == '/v2/metadata/asset_type/vocabulary') {
          $response = file_get_contents(__DIR__ . "/../../../../fixtures/assetTypeVocabulary.json");
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }

        if (preg_match('/\/api\/rest\/asset\/uuid\/(.*)\/assetversions/', $path, $matches) === 1) {
          $response = file_get_contents(__DIR__ . "/../../../../fixtures/versions/$matches[1].json");
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }

        if ($path === '/v2/categories') {
          $response = file_get_contents(__DIR__ . '/../../../../fixtures/categories.json');
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }

        if ($path === '/v2/categories/Testing') {
          return new FulfilledPromise(new Response(200, [], Json::encode([
            'items' => [],
          ])));
        }

        if ($path === '/v2/assets/assetgroups') {
          $response = file_get_contents(__DIR__ . '/../../../../fixtures/assetgroups.json');
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }

        if ($path === '/v2/user') {
          $dam_token = $this->state->get('acquia_dam_token');
          if ($dam_token && $dam_token !== 'expired_token') {
            $response = file_get_contents(__DIR__ . '/../../../../fixtures/user/user.json');
          }
          else {
            $response = file_get_contents(__DIR__ . '/../../../../fixtures/user/userInvalid.json');
          }
          assert($response !== FALSE);
          return new FulfilledPromise(new Response(200, [], $response));
        }

        $this->logger->warning(
            sprintf("The DAM client requested '%s' which is not mocked", $request->getUri())
          );
        throw new \RuntimeException('Request URI not mocked: ' . $request->getUri());
      };
    };
  }

}
