<?php

namespace Drupal\straker_translate;

use Drupal\Component\Serialization\Json;
use Drupal\straker_translate\Exception\StrakerTranslateApiException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentAlreadyCompletedException;
use Drupal\straker_translate\Exception\StrakerTranslateDocumentTargetAlreadyCompletedException;
use Drupal\straker_translate\Exception\StrakerTranslatePaymentRequiredException;
use Drupal\straker_translate\Remote\StrakerTranslateApiInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\straker_translate\Exception\StrakerTranslateProcessedWordsLimitException;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Response;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Url;

/**
 * The connecting class between Drupal and Straker Translate.
 */
class StrakerTranslate implements StrakerTranslateInterface {

  protected static $instance;
  protected $api;

  /**
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The language-locale mapper.
   *
   * @var \Drupal\straker_translate\LanguageLocaleMapperInterface
   */
  protected $languageLocaleMapper;

  /**
   * The Straker Translate configuration service.
   *
   * @var \Drupal\straker_translate\StrakerTranslateConfigurationServiceInterface
   */
  protected $straker_translateConfiguration;

  // Translation Status.
  const STATUS_EDITED = 'EDITED';
  const STATUS_IMPORTING = 'IMPORTING';
  const STATUS_NONE = 'NONE';
  const STATUS_REQUEST = 'REQUEST';
  const STATUS_PENDING = 'PENDING';
  const STATUS_INTERMEDIATE = 'INTERMEDIATE';
  const STATUS_PROCESSING = 'PROCESSING';
  const STATUS_CURRENT = 'CURRENT';
  const STATUS_READY = 'READY';
  const STATUS_DISABLED = 'DISABLED';
  const STATUS_ERROR = 'ERROR';
  const STATUS_CANCELLED = 'CANCELLED';
  const STATUS_LOCKED = 'LOCKED';

  /**
   * Status untracked means the target has not been added yet.
   */
  const STATUS_UNTRACKED = 'UNTRACKED';

  /**
   * Status DELETED means the document or target have been deleted.
   */
  const STATUS_DELETED = 'DELETED';

  /**
   * Status ARCHIVED means the document has been archived.
   */
  const STATUS_ARCHIVED = 'ARCHIVED';

  const PROGRESS_COMPLETE = 100;
  // Translation Profile.
  const PROFILE_AUTOMATIC = 'automatic';
  const PROFILE_MANUAL = 'manual';
  const PROFILE_DISABLED = 'disabled';

  const SETTINGS = 'straker_translate.settings';

  /**
   * Constructs a Straker Translate object.
   *
   * @param \Drupal\straker_translate\Remote\StrakerTranslateApiInterface $api
   *   The Straker Translate configuration service.
   * @param \Drupal\straker_translate\LanguageLocaleMapperInterface $language_locale_mapper
   *   The language-locale mapper.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(StrakerTranslateApiInterface $api, LanguageLocaleMapperInterface $language_locale_mapper, ConfigFactoryInterface $config_factory, StrakerTranslateConfigurationServiceInterface $straker_translate_configuration) {
    $this->api = $api;
    $this->languageLocaleMapper = $language_locale_mapper;
    $this->configFactory = $config_factory;
    $this->straker_translateConfiguration = $straker_translate_configuration;
  }

  /**
   *
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('straker_translate.api'),
      $container->get('straker_translate.language_locale_mapper'),
      $container->get('config.factory'),
      $container->get('straker_translate.configuration')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function get($key) {
    return $this->configFactory->get(static::SETTINGS)->get($key);
  }

  /**
   * {@inheritdoc}
   */
  public function getEditable($key) {
    return $this->configFactory->getEditable(static::SETTINGS)->get($key);
  }

  /**
   * {@inheritdoc}
   */
  public function set($key, $value) {
    $this->configFactory->getEditable(static::SETTINGS)->set($key, $value)->save();
  }

  /**
   * {@inheritdoc}
   */
  public function getAccountInfo() {
    try {
      $workflow_response = $this->api->getAccountInfo();
    }
    catch (StrakerTranslateApiException $e) {
      // @todo log a warning
      return FALSE;
    }
    if (!empty($workflow_response)) {
      return $workflow_response;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getResources($force = FALSE) {
    return [
      'workflow' => $this->getWorkflows($force),
    ];
  }

  /**
   *
   */
  public function getDefaults() {
    return $this->configFactory->get('straker_translate.account')->get('default');
  }

  /**
   * {@inheritdoc}
   */
  public function getWorkflows($force = FALSE) {
    return $this->getResource('resources.workflow', 'getWorkflows', $force);
  }

  /**
   * {@inheritdoc}
   */
  public function uploadDocument($title, $source_data, $locale, $token_confirmation, $filename = NULL, StrakerTranslateProfileInterface $profile = NULL) {
    if (!is_array($source_data)) {
      $data = json_decode($source_data, TRUE);
      // This is the quickest way if $content is not a valid json object.
      $source_data = ($data === NULL) ? $source_data : $data;
    }
    // Handle adding site defaults to the upload here, and leave
    // the handling of the upload call itself to the API.
    // Get default workflow from config.
    $workflow_id = $this->configFactory->get('straker_translate.account')->get('default.workflow');

    // Override if profile has a workflow different from 'default'.
    if ($profile && ($profile_workflow = $profile->getWorkflow()) && $profile_workflow !== 'default') {
      $workflow_id = $profile_workflow;
    }
    if ($profile && ($profile->hasAutomaticUpload() || $profile->hasAutomaticDownload())) {
      $callback_uri = Url::fromRoute('straker_translate.notify', [], ['absolute' => TRUE])->toString();
      // for local development testing with ngrok
      // $callback_uri = 'https://cfa1b3fd4457.ngrok-free.app/straker/notify';
    }

    // Stores the content data in a temporary file.
    $file_path = $this->saveTemporaryContentFile($source_data, $filename);

    $args = [
      'languages' => $locale,
      'title' => $title,
      'workflow_id' => $workflow_id,
      'confirmation_required' => $token_confirmation,
      'files' => $file_path,
      'callback_uri' => $callback_uri ?? NULL,
    ];

    $response = $this->api->addDocument($args);

    $statusCode = $response->getStatusCode();
    if ($statusCode == Response::HTTP_OK) {
      $responseBody = Json::decode($response->getBody());
      if (!empty($responseBody) && !empty($responseBody['project_id'])) {
        return $responseBody['project_id'];
      }
      else {
        return FALSE;
      }
    }
    elseif ($statusCode == Response::HTTP_PAYMENT_REQUIRED) {
      // This is only applicable to subscription-based connectors, but the
      // recommended action is to present the user with a message letting them
      // know their Straker Translate account has been disabled, and to please contact
      // support to re-enable their account.
      $responseBody = Json::decode($response->getBody());
      $message = '';
      if (!empty($responseBody) && isset($responseBody['messages'])) {
        $message = $responseBody['messages'][0];
      }
      throw new StrakerTranslatePaymentRequiredException($message);
    }
    elseif ($statusCode == Response::HTTP_TOO_MANY_REQUESTS) {
      // This only applies to trial users.
      $responseBody = Json::decode($response->getBody());
      $message = '';
      if (!empty($responseBody) && isset($responseBody['messages'])) {
        $message = $responseBody['messages'][0];
      }
      throw new StrakerTranslateProcessedWordsLimitException($message);
    }
    else {
      return FALSE;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function confirmDocument($project_id) {
    $result = FALSE;
    try {
      $response = $this->api->confirmDocument($project_id);
      $status_code = $response->getStatusCode();
      if ($status_code == Response::HTTP_OK) {
        $result = StrakerTranslate::STATUS_READY;
      }
    }
    catch (\Exception $e) {
      throw new StrakerTranslateDocumentAlreadyCompletedException($e->getMessage(), $e->getCode());
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function saveTemporaryContentFile($data, string $filename): ?string {
    try {
      $json = is_array($data)
        ? json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
        : $data;

      if ($json === FALSE) {
        throw new \UnexpectedValueException('Failed to encode data to JSON.');
      }

      $uri = 'temporary://' . $filename;
      $saved_uri = \Drupal::service('file_system')->saveData($json, $uri, FileSystemInterface::EXISTS_REPLACE);

      if ($saved_uri === FALSE) {
        throw new \RuntimeException('File could not be saved.');
      }

      return $saved_uri;
    }
    catch (\Throwable $e) {
      \Drupal::logger('straker_translate')->error('Failed to save temp file: @msg', ['@msg' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function cancelDocument($doc_id) {
    $result = FALSE;
    try {
      $response = $this->api->cancelDocument($doc_id);
      $status_code = $response->getStatusCode();
      if ($status_code == Response::HTTP_NO_CONTENT) {
        $result = TRUE;
      }
    }
    catch (StrakerTranslateApiException $ltkException) {
      if ($ltkException->getCode() === Response::HTTP_BAD_REQUEST) {
        if (strpos($ltkException->getMessage(), '"Unable to cancel documents which are already in a completed state.') >= 0) {
          throw new StrakerTranslateDocumentAlreadyCompletedException($ltkException->getMessage(), $ltkException->getCode());
        }
      }
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function cancelDocumentTarget($doc_id, $locale) {
    $result = FALSE;
    try {
      $response = $this->api->cancelDocumentTarget($doc_id, $locale);
      $status_code = $response->getStatusCode();
      if ($status_code == Response::HTTP_NO_CONTENT) {
        $result = TRUE;
      }
    }
    catch (StrakerTranslateApiException $ltkException) {
      if ($ltkException->getCode() === Response::HTTP_BAD_REQUEST) {
        if (strpos($ltkException->getMessage(), '"Unable to cancel translations which are already in a completed state.') >= 0) {
          throw new StrakerTranslateDocumentTargetAlreadyCompletedException($ltkException->getMessage(), $ltkException->getCode());
        }
      }
      throw $ltkException;
    }
    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getLocalesInfo($force = FALSE) {
    $data = $this->api->getLocales($force);
    $locales = [];
    if ($data) {
      foreach ($data['data'] as $locale) {
        $locales[$locale['id']] = [
          'id' => $locale['id'],
          'language_code' => $locale['code'],
          'title' => $locale['name'],
        ];
      }
    }
    return $locales;
  }

  /**
   *
   */
  protected function getResource($resources_key, $func, $force = FALSE) {
    $config_key = str_replace('account.', '', $resources_key);
    $data = $this->configFactory->get('straker_translate.account')->get($config_key);
    if (empty($data) || $force) {
      $data = $this->api->$func();
      // Add/update workflows from the API response.
      $this->configFactory->getEditable('straker_translate.account')->set($config_key, $data)->save();
      $keys = explode(".", $resources_key);
      $default_key = 'default.' . end($keys);
      $this->setValidDefaultIfNotSet($default_key, $data);
    }
    return $data;
  }

  /**
   *
   */
  protected function setValidDefaultIfNotSet($default_key, $resources) {
    $default_value = $this->configFactory->get('straker_translate.account')->get($default_key);
    $valid_resource_ids = array_keys($resources);
    if ($default_key === 'default.filter') {
      $valid_resource_ids[] = 'drupal_default';
    }
    if (empty($default_value) || !in_array($default_value, $valid_resource_ids)) {
      $value = current($valid_resource_ids);
      $this->configFactory->getEditable('straker_translate.account')->set($default_key, $value)->save();
    }
  }

  /**
   * {@inheritDoc}
   */
  public function getUploadedTimestamp($doc_id) {
    // For now, a passthrough to the API object so the controllers do not
    // need to include that class.
    $modified_date = FALSE;
    try {
      $response = $this->api->getDocumentInfo($doc_id);
      if ($response->getStatusCode() == Response::HTTP_OK) {
        $response_json = json_decode($response->getBody(), TRUE);
        // We have millisecond precision in Straker Translate.
        $modified_date = intval(floor($response_json['properties']['last_uploaded_date'] / 1000));
      }
    }
    catch (StrakerTranslateApiException $exception) {
      return FALSE;
    }
    return $modified_date;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentStatus($doc_id) {
    // For now, a passthrough to the API object so the controllers do not
    // need to include that class.
    try {
      $response = $this->api->getDocumentStatus($doc_id);
      if ($response->getStatusCode() == Response::HTTP_OK) {
        // If an exception didn't happen, the document is succesfully imported.
        // The status value there is related with translation status, so we must
        // ignore it.
        return json_decode($response->getBody(), TRUE);
      }
    }
    catch (StrakerTranslateApiException $exception) {
      return FALSE;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentTranslationStatus($doc_id, $locale) {
    // For now, a passthrough to the API object so the controllers do not
    // need to include that class.
    $response = $this->api->getDocumentTranslationStatus($doc_id, $locale);
    $progress = FALSE;
    $readyToDownload = FALSE;
    if ($response->getStatusCode() == Response::HTTP_OK) {
      $progress_json = json_decode($response->getBody(), TRUE);
      $straker_translate_locale = str_replace("_", "-", $locale);
      if (!empty($progress_json['entities'])) {

        foreach ($progress_json['entities'] as $index => $data) {
          if ($data['properties']['locale_code'] === $straker_translate_locale) {
            $progress = $data['properties']['percent_complete'];
            if (isset($data['properties']['ready_to_download'])) {
              // "false" should evaluate to FALSE, that's why we add the filter_var.
              $readyToDownload = is_string($data['properties']['ready_to_download']) ?
                filter_var($data['properties']['ready_to_download'], FILTER_VALIDATE_BOOLEAN) :
                (bool) $data['properties']['ready_to_download'];
            }
            if ($data['properties']['status'] === StrakerTranslate::STATUS_CANCELLED) {
              $progress = $data['properties']['status'];
              $readyToDownload = FALSE;
            }
            break;
          }
        }
      }
      if ($readyToDownload) {
        return TRUE;
      }
    }
    return $progress;
  }

  /**
   * {@inheritdoc}
   */
  public function getDocumentTranslationStatuses($doc_id) {
    $statuses = [];
    try {
      $response = $this->api->getDocumentTranslationStatuses($doc_id);
    }
    catch (StrakerTranslateApiException $e) {
      // No targets found for this doc.
      return $statuses;
    }
    if ($response->getStatusCode() == Response::HTTP_OK) {
      $progress_json = json_decode($response->getBody(), TRUE);
      if (!empty($progress_json['entities'])) {
        foreach ($progress_json['entities'] as $index => $data) {
          $straker_translate_locale = $data['properties']['locale_code'];
          $statuses[$straker_translate_locale] = $data['properties']['percent_complete'];
          // @todo We should have an structure for this, instead of treating this as an snowflake.
          if ($data['properties']['status'] === StrakerTranslate::STATUS_CANCELLED) {
            $statuses[$straker_translate_locale] = $data['properties']['status'];
          }
        }
      }
    }
    return $statuses;
  }

  /**
   * {@inheritdoc}
   */
  public function downloadDocument($doc_id, $locale) {
    // For now, a passthrough to the API object so the controllers do not
    // need to include that class.
    $response = $this->api->getTranslation($doc_id, $locale);
    $statusCode = $response->getStatusCode();
    if ($statusCode == Response::HTTP_OK) {
      return json_decode($response->getBody(), TRUE);
    }
    elseif ($statusCode == Response::HTTP_GONE) {
      // Set the status of the document back to its pre-uploaded state.
      // Typically this means the state would be set to Upload, or None but this
      // may vary depending on connector. Essentially, the content’s status
      // indicator should show that the source content needs to be re-uploaded
      // to Straker Translate.
      return FALSE;
    }
    return FALSE;
  }

}
