/** @file
  RedfishHttpDxe produces EdkIIRedfishHttpProtocol
  for EDK2 Redfish Feature driver to do HTTP operations.

  Copyright (c) 2023-2024, NVIDIA CORPORATION & AFFILIATES. All rights reserved.

  SPDX-License-Identifier: BSD-2-Clause-Patent

**/

#include "RedfishHttpDxe.h"
#include "RedfishHttpData.h"
#include "RedfishHttpOperation.h"

REDFISH_HTTP_CACHE_PRIVATE  *mRedfishHttpCachePrivate = NULL;

/**
  Debug output the cache list.

  @param[in]    Msg            Debug message string.
  @param[in]    ErrorLevel     Output error level.
  @param[in]    CacheList      Target list to dump.

  @retval EFI_SUCCESS             Debug dump finished.
  @retval EFI_INVALID_PARAMETER   HttpCacheList is NULL.

**/
EFI_STATUS
DebugPrintHttpCacheList (
  IN  CONST CHAR8              *Msg,
  IN  UINTN                    ErrorLevel,
  IN  REDFISH_HTTP_CACHE_LIST  *CacheList
  )
{
  LIST_ENTRY               *List;
  REDFISH_HTTP_CACHE_DATA  *Data;
  UINTN                    Index;

  if (CacheList == NULL) {
    return EFI_INVALID_PARAMETER;
  }

  if (!IS_EMPTY_STRING (Msg)) {
    DEBUG ((ErrorLevel, "%a\n", Msg));
  }

  if (IsListEmpty (&CacheList->Head)) {
    DEBUG ((ErrorLevel, "list is empty\n"));
    return EFI_NOT_FOUND;
  }

  DEBUG ((ErrorLevel, "list count: %d capacity: %d\n", CacheList->Count, CacheList->Capacity));
  Data  = NULL;
  Index = 0;
  List  = GetFirstNode (&CacheList->Head);
  while (!IsNull (&CacheList->Head, List)) {
    Data = REDFISH_HTTP_CACHE_FROM_LIST (List);

    DEBUG ((ErrorLevel, "%d) Uri: %s Hit: %d\n", ++Index, Data->Uri, Data->HitCount));

    List = GetNextNode (&CacheList->Head, List);
  }

  return EFI_SUCCESS;
}

/**

  Check HTTP status code to see if we like to retry HTTP request or not.

  @param[in]  StatusCode      HTTP status code.

  @retval     BOOLEAN         Return true when we like to retry request.
                              Return false when we don't want to retry request.

**/
BOOLEAN
RedfishRetryRequired (
  IN EFI_HTTP_STATUS_CODE  *StatusCode
  )
{
  if (StatusCode == NULL) {
    return TRUE;
  }

  if ((*StatusCode == HTTP_STATUS_500_INTERNAL_SERVER_ERROR) ||
      (*StatusCode == HTTP_STATUS_UNSUPPORTED_STATUS))
  {
    return TRUE;
  }

  return FALSE;
}

/**

  Convert Unicode string to ASCII string. It's call responsibility to release returned buffer.

  @param[in]  UnicodeStr      Unicode string to convert.

  @retval     CHAR8 *         ASCII string returned.
  @retval     NULL            Errors occur.

**/
CHAR8 *
StringUnicodeToAscii (
  IN EFI_STRING  UnicodeStr
  )
{
  CHAR8       *AsciiStr;
  UINTN       AsciiStrSize;
  EFI_STATUS  Status;

  if (IS_EMPTY_STRING (UnicodeStr)) {
    return NULL;
  }

  AsciiStrSize = StrLen (UnicodeStr) + 1;
  AsciiStr     = AllocateZeroPool (AsciiStrSize);
  if (AsciiStr == NULL) {
    return NULL;
  }

  Status = UnicodeStrToAsciiStrS (UnicodeStr, AsciiStr, AsciiStrSize);
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "UnicodeStrToAsciiStrS failed: %r\n", Status));
    FreePool (AsciiStr);
    return NULL;
  }

  return AsciiStr;
}

/**
  Return HTTP method in ASCII string. Caller does not need
  to free returned string buffer.

  @param[in]  Method         HTTP method.

  @retval CHAR8 *   Method in string.
**/
CHAR8 *
HttpMethodToString (
  IN  EFI_HTTP_METHOD  Method
  )
{
  switch (Method) {
    case HttpMethodGet:
      return HTTP_METHOD_GET;
      break;
    case HttpMethodPost:
      return HTTP_METHOD_POST;
      break;
    case HttpMethodPatch:
      return HTTP_METHOD_PATCH;
      break;
    case HttpMethodPut:
      return HTTP_METHOD_PUT;
      break;
    case HttpMethodDelete:
      return HTTP_METHOD_DELETE;
      break;
    default:
      break;
  }

  return "Unknown";
}

/**
  Report HTTP communication error via report status code.

  @param[in]  Method         HTTP method.
  @param[in]  Uri            The URI which has failure.
  @param[in]  HttpStatusCode HTTP status code.

**/
VOID
ReportHttpError (
  IN  EFI_HTTP_METHOD       Method,
  IN  EFI_STRING            Uri,
  IN  EFI_HTTP_STATUS_CODE  *HttpStatusCode  OPTIONAL
  )
{
  CHAR8  ErrorMsg[REDFISH_ERROR_MSG_MAX];

  if (IS_EMPTY_STRING (Uri)) {
    DEBUG ((DEBUG_ERROR, "%a: no URI to report error status\n", __func__));
    return;
  }

  //
  // Report failure of URI and HTTP status code.
  //
  AsciiSPrint (ErrorMsg, sizeof (ErrorMsg), REDFISH_HTTP_ERROR_REPORT, HttpMethodToString (Method), (HttpStatusCode == NULL ? HTTP_STATUS_UNSUPPORTED_STATUS : *HttpStatusCode), Uri);
  DEBUG ((DEBUG_ERROR, "%a\n", ErrorMsg));
  //
  // TODO:
  // Below PI status code is approved by PIWG and wait for specification published.
  // We will uncomment below report status code after PI status code get published.
  // REF: https://bugzilla.tianocore.org/show_bug.cgi?id=4483
  //
  // REPORT_STATUS_CODE_WITH_EXTENDED_DATA (
  //  EFI_ERROR_CODE | EFI_ERROR_MAJOR,
  //  EFI_COMPUTING_UNIT_MANAGEABILITY | EFI_MANAGEABILITY_EC_REDFISH_COMMUNICATION_ERROR,
  //  ErrorMsg,
  //  AsciiStrSize (ErrorMsg)
  //  );
}

/**
  This function create Redfish service. It's caller's responsibility to free returned
  Redfish service by calling FreeService ().

  @param[in]  This                       Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  RedfishConfigServiceInfo   Redfish config service information.

  @retval     REDFISH_SERVICE  Redfish service is created.
  @retval     NULL             Errors occur.

**/
REDFISH_SERVICE
EFIAPI
RedfishCreateRedfishService (
  IN  EDKII_REDFISH_HTTP_PROTOCOL         *This,
  IN  REDFISH_CONFIG_SERVICE_INFORMATION  *RedfishConfigServiceInfo
  )
{
  EFI_STATUS                  Status;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;
  REDFISH_SERVICE_PRIVATE     *NewService;
  CHAR8                       *AsciiLocation;
  CHAR8                       *Host;
  CHAR8                       *BasicAuthString;
  UINTN                       BasicAuthStrSize;
  CHAR8                       *EncodedAuthString;
  UINTN                       EncodedAuthStrSize;
  EDKII_REDFISH_AUTH_METHOD   AuthMethod;
  CHAR8                       *Username;
  CHAR8                       *Password;
  UINTN                       UsernameSize;
  UINTN                       PasswordSize;
  EFI_REST_EX_PROTOCOL        *RestEx;

  if ((This == NULL) || (RedfishConfigServiceInfo == NULL)) {
    return NULL;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: service location: %s\n", __func__, RedfishConfigServiceInfo->RedfishServiceLocation));

  Private            = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);
  BasicAuthString    = NULL;
  EncodedAuthString  = NULL;
  Username           = NULL;
  Password           = NULL;
  NewService         = NULL;
  AsciiLocation      = NULL;
  Host               = NULL;
  BasicAuthStrSize   = 0;
  EncodedAuthStrSize = 0;
  UsernameSize       = 0;
  PasswordSize       = 0;

  //
  // Build host and host name from service location
  //
  if (!IS_EMPTY_STRING (RedfishConfigServiceInfo->RedfishServiceLocation)) {
    AsciiLocation = StringUnicodeToAscii (RedfishConfigServiceInfo->RedfishServiceLocation);
    if (AsciiLocation == NULL) {
      goto ON_RELEASE;
    }

    Host = AllocateZeroPool (REDFISH_HOST_NAME_MAX);
    if (AsciiLocation == NULL) {
      goto ON_RELEASE;
    }

    if (RedfishConfigServiceInfo->RedfishServiceUseHttps) {
      AsciiSPrint (Host, REDFISH_HOST_NAME_MAX, "https://%a", AsciiLocation);
    } else {
      AsciiSPrint (Host, REDFISH_HOST_NAME_MAX, "http://%a", AsciiLocation);
    }

    DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Host: %a\n", __func__, Host));
  }

  //
  // Find Rest Ex protocol
  //
  if (RedfishConfigServiceInfo->RedfishServiceRestExHandle != NULL) {
    Status = gBS->HandleProtocol (
                    RedfishConfigServiceInfo->RedfishServiceRestExHandle,
                    &gEfiRestExProtocolGuid,
                    (VOID **)&RestEx
                    );
  } else {
    DEBUG ((DEBUG_ERROR, "%a: Rest Ex protocol is not available\n", __func__));
    goto ON_RELEASE;
  }

  //
  // Get credential
  //
  if (Private->CredentialProtocol == NULL) {
    //
    // No credential available on this system.
    //
    DEBUG ((DEBUG_WARN, "%a: no credential protocol available\n", __func__));
  } else {
    Status = Private->CredentialProtocol->GetAuthInfo (
                                            Private->CredentialProtocol,
                                            &AuthMethod,
                                            &Username,
                                            &Password
                                            );
    if (EFI_ERROR (Status) || ((AuthMethod != AuthMethodNone) && (IS_EMPTY_STRING (Username) || IS_EMPTY_STRING (Password)))) {
      DEBUG ((DEBUG_ERROR, "%a: cannot get authentication information: %r\n", __func__, Status));
      goto ON_RELEASE;
    } else if (AuthMethod != AuthMethodNone) {
      DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Auth method: 0x%x username: %a password: %a\n", __func__, AuthMethod, Username, Password));

      //
      // Perform base64 encoding (RFC 7617)
      //
      UsernameSize     = AsciiStrSize (Username);
      PasswordSize     = AsciiStrSize (Password);
      BasicAuthStrSize =  UsernameSize + PasswordSize;  // one byte taken from null-terminator for ':'
      BasicAuthString  = AllocateZeroPool (BasicAuthStrSize);
      if (BasicAuthString == NULL) {
        goto ON_RELEASE;
      }

      AsciiSPrint (
        BasicAuthString,
        BasicAuthStrSize,
        "%a:%a",
        Username,
        Password
        );

      Status = Base64Encode (
                 (CONST UINT8 *)BasicAuthString,
                 BasicAuthStrSize,
                 EncodedAuthString,
                 &EncodedAuthStrSize
                 );
      if ((Status == EFI_BUFFER_TOO_SMALL) && (EncodedAuthStrSize > 0)) {
        EncodedAuthString = AllocateZeroPool (EncodedAuthStrSize);
        if (EncodedAuthString == NULL) {
          goto ON_RELEASE;
        }

        Status = Base64Encode (
                   (CONST UINT8 *)BasicAuthString,
                   BasicAuthStrSize,
                   EncodedAuthString,
                   &EncodedAuthStrSize
                   );
        if (EFI_ERROR (Status)) {
          DEBUG ((DEBUG_ERROR, "%a: Base64Encode failure: %r\n", __func__, Status));
        }

        DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Basic authorization: %a\n", __func__, EncodedAuthString));
      } else {
        DEBUG ((DEBUG_ERROR, "%a: Base64Encode failure: %r\n", __func__, Status));
        goto ON_RELEASE;
      }
    }
  }

  NewService = CreateRedfishService (Host, AsciiLocation, EncodedAuthString, NULL, RestEx);
  if (NewService == NULL) {
    DEBUG ((DEBUG_ERROR, "%a: CreateRedfishService\n", __func__));
    goto ON_RELEASE;
  }

  if (Private->CredentialProtocol != NULL) {
    Status = Private->CredentialProtocol->RegisterRedfishService (Private->CredentialProtocol, NewService);
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a: Failed to register Redfish service - %r\n", __func__, Status));
    }
  }

ON_RELEASE:

  if (BasicAuthString != NULL) {
    ZeroMem (BasicAuthString, BasicAuthStrSize);
    FreePool (BasicAuthString);
  }

  if (EncodedAuthString != NULL) {
    ZeroMem (BasicAuthString, EncodedAuthStrSize);
    FreePool (EncodedAuthString);
  }

  if (Username != NULL) {
    ZeroMem (Username, UsernameSize);
    FreePool (Username);
  }

  if (Password != NULL) {
    ZeroMem (Password, PasswordSize);
    FreePool (Password);
  }

  if (AsciiLocation != NULL) {
    FreePool (AsciiLocation);
  }

  if (Host != NULL) {
    FreePool (Host);
  }

  return NewService;
}

/**
  This function free resources in Redfish service. RedfishService is no longer available
  after this function returns successfully.

  @param[in]  This            Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  RedfishService  Pointer to Redfish service to be released.

  @retval     EFI_SUCCESS     Resource is released successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishFreeRedfishService (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_SERVICE              RedfishService
  )
{
  EFI_STATUS                  Status;
  REDFISH_SERVICE_PRIVATE     *Service;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  if ((This == NULL) || (RedfishService == NULL)) {
    return EFI_INVALID_PARAMETER;
  }

  Private = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);

  Service = (REDFISH_SERVICE_PRIVATE *)RedfishService;
  if (Service->Signature != REDFISH_HTTP_SERVICE_SIGNATURE) {
    DEBUG ((DEBUG_ERROR, "%a: signature check failure\n", __func__));
  }

  if (Private->CredentialProtocol != NULL) {
    Status = Private->CredentialProtocol->UnregisterRedfishService (Private->CredentialProtocol, RedfishService);
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a: Failed to unregister Redfish service - %r\n", __func__, Status));
    } else {
      if (Service->RestEx != NULL) {
        Status = Service->RestEx->Configure (Service->RestEx, NULL);
        DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: release RestEx instance: %r\n", __func__, Status));
      }
    }
  }

  return ReleaseRedfishService (Service);
}

/**
  This function returns JSON value in given RedfishPayload. Returned JSON value
  is a reference to the JSON value in RedfishPayload. Any modification to returned
  JSON value will change JSON value in RedfishPayload.

  @param[in]  This            Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  RedfishPayload  Pointer to Redfish payload.

  @retval     EDKII_JSON_VALUE   JSON value is returned.
  @retval     NULL               Errors occur.

**/
EDKII_JSON_VALUE
EFIAPI
RedfishJsonInRedfishPayload (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_PAYLOAD              RedfishPayload
  )
{
  REDFISH_PAYLOAD_PRIVATE  *Payload;

  if ((This == NULL) || (RedfishPayload == NULL)) {
    return NULL;
  }

  Payload = (REDFISH_PAYLOAD_PRIVATE *)RedfishPayload;
  if (Payload->Signature != REDFISH_HTTP_PAYLOAD_SIGNATURE) {
    DEBUG ((DEBUG_ERROR, "%a: signature check failure\n", __func__));
  }

  return Payload->JsonValue;
}

/**
  Perform HTTP GET to Get redfish resource from given resource URI with
  cache mechanism supported. It's caller's responsibility to free Response
  by calling FreeResponse ().

  @param[in]  This          Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Service       Redfish service instance to perform HTTP GET.
  @param[in]  Uri           Target resource URI.
  @param[in]  Request       Additional request context. This is optional.
  @param[out] Response      HTTP response from redfish service.
  @param[in]  UseCache      If it is TRUE, this function will search for
                            cache first. If it is FALSE, this function
                            will query Redfish URI directly.

  @retval     EFI_SUCCESS     Resource is returned successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishGetResource (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_SERVICE              Service,
  IN  EFI_STRING                   Uri,
  IN  REDFISH_REQUEST              *Request OPTIONAL,
  OUT REDFISH_RESPONSE             *Response,
  IN  BOOLEAN                      UseCache
  )
{
  EFI_STATUS                  Status;
  REDFISH_HTTP_CACHE_DATA     *CacheData;
  UINTN                       RetryCount;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  if ((This == NULL) || (Service == NULL) || (Response == NULL) || IS_EMPTY_STRING (Uri)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Get URI: %s cache: %a\n", __func__, Uri, (UseCache ? "true" : "false")));

  Private    = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);
  CacheData  = NULL;
  RetryCount = 0;
  ZeroMem (Response, sizeof (REDFISH_RESPONSE));

  if (Private->CacheDisabled) {
    UseCache = FALSE;
    DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: cache is disabled by PCD!\n", __func__));
  }

  //
  // Search for cache list.
  //
  if (UseCache) {
    CacheData = FindHttpCacheData (&Private->CacheList.Head, Uri);
    if (CacheData != NULL) {
      DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: cache hit! %s\n", __func__, Uri));

      //
      // Copy cached response to caller's buffer.
      //
      Status               = CopyRedfishResponse (CacheData->Response, Response);
      CacheData->HitCount += 1;
      return Status;
    }
  }

  //
  // Get resource from redfish service.
  //
  do {
    RetryCount += 1;
    Status      = HttpSendReceive (
                    Service,
                    Uri,
                    HttpMethodGet,
                    Request,
                    Response
                    );
    DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: HTTP request: %s :%r\n", __func__, Uri, Status));
    if (!EFI_ERROR (Status) || (RetryCount >= Private->RetrySetting.MaximumRetryGet)) {
      break;
    }

    //
    // Retry when BMC is not ready.
    //
    if ((Response->StatusCode != NULL)) {
      DEBUG_CODE (
        DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
        );

      if (!RedfishRetryRequired (Response->StatusCode)) {
        break;
      }

      //
      // Release response for next round of request.
      //
      This->FreeResponse (This, Response);
    }

    DEBUG ((DEBUG_WARN, "%a: RedfishGetByUriEx failed, retry (%d/%d)\n", __func__, RetryCount, Private->RetrySetting.MaximumRetryGet));
    if (Private->RetrySetting.RetryWait > 0) {
      gBS->Stall (Private->RetrySetting.RetryWait);
    }
  } while (TRUE);

  if (EFI_ERROR (Status)) {
    DEBUG_CODE (
      DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
      );
    //
    // Report status code for Redfish failure
    //
    ReportHttpError (HttpMethodGet, Uri, Response->StatusCode);
    DEBUG ((DEBUG_ERROR, "%a: get %s failed (%d/%d): %r\n", __func__, Uri, RetryCount, Private->RetrySetting.MaximumRetryGet, Status));
    goto ON_RELEASE;
  }

  if (!Private->CacheDisabled) {
    //
    // Keep response in cache list
    //
    Status = AddHttpCacheData (&Private->CacheList, Uri, Response);
    if (EFI_ERROR (Status)) {
      DEBUG ((DEBUG_ERROR, "%a: failed to cache %s: %r\n", __func__, Uri, Status));
      goto ON_RELEASE;
    }

    DEBUG_CODE (
      DebugPrintHttpCacheList (__func__, REDFISH_HTTP_CACHE_DEBUG_DUMP, &Private->CacheList);
      );
  }

ON_RELEASE:

  return Status;
}

/**
  This function free resources in Request. Request is no longer available
  after this function returns successfully.

  @param[in]  This         Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Request      HTTP request to be released.

  @retval     EFI_SUCCESS     Resource is released successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishFreeRequest (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_REQUEST              *Request
  )
{
  if ((This == NULL) || (Request == NULL)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: entry\n", __func__));

  return ReleaseRedfishRequest (Request);
}

/**
  This function free resources in given Response.

  @param[in]  This         Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Response     HTTP response to be released.

  @retval     EFI_SUCCESS     Resource is released successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishFreeResponse (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_RESPONSE             *Response
  )
{
  if ((This == NULL) || (Response == NULL)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: entry\n", __func__));

  return ReleaseRedfishResponse (Response);
}

/**
  This function expire the cached response of given URI.

  @param[in]  This         Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Uri          Target response of URI.

  @retval     EFI_SUCCESS     Target response is expired successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishExpireResponse (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  EFI_STRING                   Uri
  )
{
  REDFISH_HTTP_CACHE_PRIVATE  *Private;
  REDFISH_HTTP_CACHE_DATA     *CacheData;

  if ((This == NULL) || IS_EMPTY_STRING (Uri)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: expire URI: %s\n", __func__, Uri));

  Private = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);

  CacheData = FindHttpCacheData (&Private->CacheList.Head, Uri);
  if (CacheData == NULL) {
    return EFI_NOT_FOUND;
  }

  return DeleteHttpCacheData (&Private->CacheList, CacheData);
}

/**
  Perform HTTP PATCH to send redfish resource to given resource URI.
  It's caller's responsibility to free Response by calling FreeResponse ().

  @param[in]  This          Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Service       Redfish service instance to perform HTTP PATCH.
  @param[in]  Uri           Target resource URI.
  @param[in]  Content       Data to patch.
  @param[in]  ContentSize   Size of the Content to be send to Redfish service.
                            This is optional. When ContentSize is 0, ContentSize
                            is the size of Content.
  @param[in]  ContentType   Type of the Content to be send to Redfish service.
                            This is optional. When ContentType is NULL, content
                            type HTTP_CONTENT_TYPE_APP_JSON will be used.
  @param[out] Response      HTTP response from redfish service.

  @retval     EFI_SUCCESS     Resource is returned successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishPatchResource (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_SERVICE              Service,
  IN  EFI_STRING                   Uri,
  IN  CHAR8                        *Content,
  IN  UINTN                        ContentSize OPTIONAL,
  IN  CHAR8                        *ContentType OPTIONAL,
  OUT REDFISH_RESPONSE             *Response
  )
{
  EFI_STATUS                  Status;
  UINTN                       RetryCount;
  REDFISH_REQUEST             Request;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  if ((This == NULL) || (Service == NULL) || (Response == NULL) || IS_EMPTY_STRING (Uri) || IS_EMPTY_STRING (Content)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Patch URI: %s\n", __func__, Uri));

  Private    = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);
  RetryCount = 0;
  ZeroMem (Response, sizeof (REDFISH_RESPONSE));
  ZeroMem (&Request, sizeof (REDFISH_REQUEST));

  Request.Content       = Content;
  Request.ContentLength = ContentSize;
  Request.ContentType   = ContentType;

  //
  // Patch resource to redfish service.
  //
  do {
    RetryCount += 1;
    Status      = HttpSendReceive (
                    Service,
                    Uri,
                    HttpMethodPatch,
                    &Request,
                    Response
                    );
    DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: HTTP request: %s :%r\n", __func__, Uri, Status));
    if (!EFI_ERROR (Status) || (RetryCount >= Private->RetrySetting.MaximumRetryPatch)) {
      break;
    }

    //
    // Retry when BMC is not ready.
    //
    if ((Response->StatusCode != NULL)) {
      DEBUG_CODE (
        DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
        );

      if (!RedfishRetryRequired (Response->StatusCode)) {
        break;
      }

      //
      // Release response for next round of request.
      //
      This->FreeResponse (This, Response);
    }

    DEBUG ((DEBUG_WARN, "%a: RedfishPatchToUriEx failed, retry (%d/%d)\n", __func__, RetryCount, Private->RetrySetting.MaximumRetryPatch));
    if (Private->RetrySetting.RetryWait > 0) {
      gBS->Stall (Private->RetrySetting.RetryWait);
    }
  } while (TRUE);

  //
  // Redfish resource is updated. Automatically expire the cached response
  // so application can directly get resource from Redfish service again.
  //
  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Resource is updated, expire URI: %s\n", __func__, Uri));
  RedfishExpireResponse (This, Uri);

  if (EFI_ERROR (Status)) {
    DEBUG_CODE (
      DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
      );
    //
    // Report status code for Redfish failure
    //
    ReportHttpError (HttpMethodPatch, Uri, Response->StatusCode);
    DEBUG ((DEBUG_ERROR, "%a: patch %s failed (%d/%d): %r\n", __func__, Uri, RetryCount, Private->RetrySetting.MaximumRetryPatch, Status));
    goto ON_RELEASE;
  }

ON_RELEASE:

  return Status;
}

/**
  Perform HTTP PUT to send redfish resource to given resource URI.
  It's caller's responsibility to free Response by calling FreeResponse ().

  @param[in]  This          Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Service       Redfish service instance to perform HTTP PUT.
  @param[in]  Uri           Target resource URI.
  @param[in]  Content       Data to put.
  @param[in]  ContentSize   Size of the Content to be send to Redfish service.
                            This is optional. When ContentSize is 0, ContentSize
                            is the size of Content.
  @param[in]  ContentType   Type of the Content to be send to Redfish service.
                            This is optional. When ContentType is NULL, content
                            type HTTP_CONTENT_TYPE_APP_JSON will be used.
  @param[out] Response      HTTP response from redfish service.

  @retval     EFI_SUCCESS     Resource is returned successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishPutResource (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_SERVICE              Service,
  IN  EFI_STRING                   Uri,
  IN  CHAR8                        *Content,
  IN  UINTN                        ContentSize OPTIONAL,
  IN  CHAR8                        *ContentType OPTIONAL,
  OUT REDFISH_RESPONSE             *Response
  )
{
  EFI_STATUS                  Status;
  UINTN                       RetryCount;
  REDFISH_REQUEST             Request;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  if ((This == NULL) || (Service == NULL) || (Response == NULL) || IS_EMPTY_STRING (Uri) || IS_EMPTY_STRING (Content)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Put URI: %s\n", __func__, Uri));

  Private    = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);
  RetryCount = 0;
  ZeroMem (Response, sizeof (REDFISH_RESPONSE));
  ZeroMem (&Request, sizeof (REDFISH_REQUEST));

  Request.Content       = Content;
  Request.ContentLength = ContentSize;
  Request.ContentType   = ContentType;

  //
  // Patch resource to redfish service.
  //
  do {
    RetryCount += 1;
    Status      = HttpSendReceive (
                    Service,
                    Uri,
                    HttpMethodPut,
                    &Request,
                    Response
                    );
    DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: HTTP request: %s :%r\n", __func__, Uri, Status));
    if (!EFI_ERROR (Status) || (RetryCount >= Private->RetrySetting.MaximumRetryPut)) {
      break;
    }

    //
    // Retry when BMC is not ready.
    //
    if ((Response->StatusCode != NULL)) {
      DEBUG_CODE (
        DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
        );

      if (!RedfishRetryRequired (Response->StatusCode)) {
        break;
      }

      //
      // Release response for next round of request.
      //
      This->FreeResponse (This, Response);
    }

    DEBUG ((DEBUG_WARN, "%a: RedfishPutToUri failed, retry (%d/%d)\n", __func__, RetryCount, Private->RetrySetting.MaximumRetryPut));
    if (Private->RetrySetting.RetryWait > 0) {
      gBS->Stall (Private->RetrySetting.RetryWait);
    }
  } while (TRUE);

  //
  // Redfish resource is updated. Automatically expire the cached response
  // so application can directly get resource from Redfish service again.
  //
  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Resource is updated, expire URI: %s\n", __func__, Uri));
  RedfishExpireResponse (This, Uri);

  if (EFI_ERROR (Status)) {
    DEBUG_CODE (
      DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
      );
    //
    // Report status code for Redfish failure
    //
    ReportHttpError (HttpMethodPut, Uri, Response->StatusCode);
    DEBUG ((DEBUG_ERROR, "%a: put %s failed (%d/%d): %r\n", __func__, Uri, RetryCount, Private->RetrySetting.MaximumRetryPut, Status));
    goto ON_RELEASE;
  }

ON_RELEASE:

  return Status;
}

/**
  Perform HTTP POST to send redfish resource to given resource URI.
  It's caller's responsibility to free Response by calling FreeResponse ().

  @param[in]  This          Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Service       Redfish service instance to perform HTTP POST.
  @param[in]  Uri           Target resource URI.
  @param[in]  Content       Data to post.
  @param[in]  ContentSize   Size of the Content to be send to Redfish service.
                            This is optional. When ContentSize is 0, ContentSize
                            is the size of Content.
  @param[in]  ContentType   Type of the Content to be send to Redfish service.
                            This is optional. When ContentType is NULL, content
                            type HTTP_CONTENT_TYPE_APP_JSON will be used.
  @param[out] Response      HTTP response from redfish service.

  @retval     EFI_SUCCESS     Resource is returned successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishPostResource (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_SERVICE              Service,
  IN  EFI_STRING                   Uri,
  IN  CHAR8                        *Content,
  IN  UINTN                        ContentSize OPTIONAL,
  IN  CHAR8                        *ContentType OPTIONAL,
  OUT REDFISH_RESPONSE             *Response
  )
{
  EFI_STATUS                  Status;
  UINTN                       RetryCount;
  REDFISH_REQUEST             Request;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  if ((This == NULL) || (Service == NULL) || (Response == NULL) || IS_EMPTY_STRING (Uri) || IS_EMPTY_STRING (Content)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Post URI: %s\n", __func__, Uri));

  Private    = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);
  RetryCount = 0;
  ZeroMem (Response, sizeof (REDFISH_RESPONSE));
  ZeroMem (&Request, sizeof (REDFISH_REQUEST));

  Request.Content       = Content;
  Request.ContentLength = ContentSize;
  Request.ContentType   = ContentType;

  //
  // Patch resource to redfish service.
  //
  do {
    RetryCount += 1;
    Status      = HttpSendReceive (
                    Service,
                    Uri,
                    HttpMethodPost,
                    &Request,
                    Response
                    );
    DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: HTTP request: %s :%r\n", __func__, Uri, Status));
    if (!EFI_ERROR (Status) || (RetryCount >= Private->RetrySetting.MaximumRetryPost)) {
      break;
    }

    //
    // Retry when BMC is not ready.
    //
    if ((Response->StatusCode != NULL)) {
      DEBUG_CODE (
        DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
        );

      if (!RedfishRetryRequired (Response->StatusCode)) {
        break;
      }

      //
      // Release response for next round of request.
      //
      This->FreeResponse (This, Response);
    }

    DEBUG ((DEBUG_WARN, "%a: RedfishPostToUri failed, retry (%d/%d)\n", __func__, RetryCount, Private->RetrySetting.MaximumRetryPost));
    if (Private->RetrySetting.RetryWait > 0) {
      gBS->Stall (Private->RetrySetting.RetryWait);
    }
  } while (TRUE);

  //
  // Redfish resource is updated. Automatically expire the cached response
  // so application can directly get resource from Redfish service again.
  //
  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Resource is updated, expire URI: %s\n", __func__, Uri));
  RedfishExpireResponse (This, Uri);

  if (EFI_ERROR (Status)) {
    DEBUG_CODE (
      DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
      );
    //
    // Report status code for Redfish failure
    //
    ReportHttpError (HttpMethodPost, Uri, Response->StatusCode);
    DEBUG ((DEBUG_ERROR, "%a: post %s failed (%d/%d): %r\n", __func__, Uri, RetryCount, Private->RetrySetting.MaximumRetryPost, Status));
    goto ON_RELEASE;
  }

ON_RELEASE:

  return Status;
}

/**
  Perform HTTP DELETE to delete redfish resource on given resource URI.
  It's caller's responsibility to free Response by calling FreeResponse ().

  @param[in]  This          Pointer to EDKII_REDFISH_HTTP_PROTOCOL instance.
  @param[in]  Service       Redfish service instance to perform HTTP DELETE.
  @param[in]  Uri           Target resource URI.
  @param[in]  Content       JSON represented properties to be deleted. This is
                            optional.
  @param[in]  ContentSize   Size of the Content to be send to Redfish service.
                            This is optional. When ContentSize is 0, ContentSize
                            is the size of Content if Content is not NULL.
  @param[in]  ContentType   Type of the Content to be send to Redfish service.
                            This is optional. When Content is not NULL and
                            ContentType is NULL, content type HTTP_CONTENT_TYPE_APP_JSON
                            will be used.
  @param[out] Response      HTTP response from redfish service.

  @retval     EFI_SUCCESS     Resource is returned successfully.
  @retval     Others          Errors occur.

**/
EFI_STATUS
EFIAPI
RedfishDeleteResource (
  IN  EDKII_REDFISH_HTTP_PROTOCOL  *This,
  IN  REDFISH_SERVICE              Service,
  IN  EFI_STRING                   Uri,
  IN  CHAR8                        *Content OPTIONAL,
  IN  UINTN                        ContentSize OPTIONAL,
  IN  CHAR8                        *ContentType OPTIONAL,
  OUT REDFISH_RESPONSE             *Response
  )
{
  EFI_STATUS                  Status;
  UINTN                       RetryCount;
  REDFISH_REQUEST             Request;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  if ((This == NULL) || (Service == NULL) || (Response == NULL) || IS_EMPTY_STRING (Uri)) {
    return EFI_INVALID_PARAMETER;
  }

  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Delete URI: %s\n", __func__, Uri));

  Private    = REDFISH_HTTP_CACHE_PRIVATE_FROM_THIS (This);
  RetryCount = 0;
  ZeroMem (Response, sizeof (REDFISH_RESPONSE));
  ZeroMem (&Request, sizeof (REDFISH_REQUEST));

  Request.Content       = Content;
  Request.ContentLength = ContentSize;
  Request.ContentType   = ContentType;

  //
  // Patch resource to redfish service.
  //
  do {
    RetryCount += 1;
    Status      = HttpSendReceive (
                    Service,
                    Uri,
                    HttpMethodDelete,
                    &Request,
                    Response
                    );
    DEBUG ((REDFISH_HTTP_CACHE_DEBUG_REQUEST, "%a: HTTP request: %s :%r\n", __func__, Uri, Status));
    if (!EFI_ERROR (Status) || (RetryCount >= Private->RetrySetting.MaximumRetryDelete)) {
      break;
    }

    //
    // Retry when BMC is not ready.
    //
    if ((Response->StatusCode != NULL)) {
      DEBUG_CODE (
        DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
        );

      if (!RedfishRetryRequired (Response->StatusCode)) {
        break;
      }

      //
      // Release response for next round of request.
      //
      This->FreeResponse (This, Response);
    }

    DEBUG ((DEBUG_WARN, "%a: RedfishDeleteByUri failed, retry (%d/%d)\n", __func__, RetryCount, Private->RetrySetting.MaximumRetryDelete));
    if (Private->RetrySetting.RetryWait > 0) {
      gBS->Stall (Private->RetrySetting.RetryWait);
    }
  } while (TRUE);

  //
  // Redfish resource is updated. Automatically expire the cached response
  // so application can directly get resource from Redfish service again.
  //
  DEBUG ((REDFISH_HTTP_CACHE_DEBUG, "%a: Resource is updated, expire URI: %s\n", __func__, Uri));
  RedfishExpireResponse (This, Uri);

  if (EFI_ERROR (Status)) {
    DEBUG_CODE (
      DumpRedfishResponse (NULL, DEBUG_ERROR, Response);
      );
    //
    // Report status code for Redfish failure
    //
    ReportHttpError (HttpMethodDelete, Uri, Response->StatusCode);
    DEBUG ((DEBUG_ERROR, "%a: delete %s failed (%d/%d): %r\n", __func__, Uri, RetryCount, Private->RetrySetting.MaximumRetryDelete, Status));
    goto ON_RELEASE;
  }

ON_RELEASE:

  return Status;
}

EDKII_REDFISH_HTTP_PROTOCOL  mEdkIIRedfishHttpProtocol = {
  EDKII_REDFISH_HTTP_PROTOCOL_REVISION,
  RedfishCreateRedfishService,
  RedfishFreeRedfishService,
  RedfishJsonInRedfishPayload,
  RedfishGetResource,
  RedfishPatchResource,
  RedfishPutResource,
  RedfishPostResource,
  RedfishDeleteResource,
  RedfishFreeRequest,
  RedfishFreeResponse,
  RedfishExpireResponse
};

/**
  Unloads an image.

  @param[in]  ImageHandle         Handle that identifies the image to be unloaded.

  @retval EFI_SUCCESS             The image has been unloaded.
  @retval EFI_INVALID_PARAMETER   ImageHandle is not a valid image handle.

**/
EFI_STATUS
EFIAPI
RedfishHttpDriverUnload (
  IN EFI_HANDLE  ImageHandle
  )
{
  if (mRedfishHttpCachePrivate == NULL) {
    return EFI_SUCCESS;
  }

  if (!IsListEmpty (&mRedfishHttpCachePrivate->CacheList.Head)) {
    ReleaseCacheList (&mRedfishHttpCachePrivate->CacheList);
  }

  gBS->UninstallMultipleProtocolInterfaces (
         ImageHandle,
         &gEdkIIRedfishHttpProtocolGuid,
         &mRedfishHttpCachePrivate->Protocol,
         NULL
         );

  FreePool (mRedfishHttpCachePrivate);
  mRedfishHttpCachePrivate = NULL;

  return EFI_SUCCESS;
}

/**
  This is a EDKII_REDFISH_CREDENTIAL_PROTOCOL notification event handler.

  @param[in] Event    Event whose notification function is being invoked.
  @param[in] Context  Pointer to the notification function's context.

**/
VOID
EFIAPI
CredentialProtocolInstalled (
  IN  EFI_EVENT  Event,
  IN  VOID       *Context
  )
{
  EFI_STATUS                  Status;
  REDFISH_HTTP_CACHE_PRIVATE  *Private;

  Private = (REDFISH_HTTP_CACHE_PRIVATE *)Context;
  if (Private->Signature != REDFISH_HTTP_DRIVER_SIGNATURE) {
    DEBUG ((DEBUG_ERROR, "%a: signature check failure\n", __func__));
    return;
  }

  //
  // Locate HII credential protocol.
  //
  Status = gBS->LocateProtocol (
                  &gEdkIIRedfishCredential2ProtocolGuid,
                  NULL,
                  (VOID **)&Private->CredentialProtocol
                  );
  if (EFI_ERROR (Status)) {
    return;
  }

  gBS->CloseEvent (Event);
}

/**
  Main entry for this driver.

  @param[in] ImageHandle     Image handle this driver.
  @param[in] SystemTable     Pointer to SystemTable.

  @retval EFI_SUCCESS     This function always complete successfully.

**/
EFI_STATUS
EFIAPI
RedfishHttpEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS  Status;
  VOID        *Registration;

  if (mRedfishHttpCachePrivate != NULL) {
    return EFI_ALREADY_STARTED;
  }

  mRedfishHttpCachePrivate = AllocateZeroPool (sizeof (REDFISH_HTTP_CACHE_PRIVATE));
  if (mRedfishHttpCachePrivate == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  //
  // Initial cache list and protocol instance.
  //
  mRedfishHttpCachePrivate->Signature   = REDFISH_HTTP_DRIVER_SIGNATURE;
  mRedfishHttpCachePrivate->ImageHandle = ImageHandle;
  CopyMem (&mRedfishHttpCachePrivate->Protocol, &mEdkIIRedfishHttpProtocol, sizeof (EDKII_REDFISH_HTTP_PROTOCOL));
  mRedfishHttpCachePrivate->CacheList.Capacity = REDFISH_HTTP_CACHE_LIST_SIZE;
  mRedfishHttpCachePrivate->CacheList.Count    = 0x00;
  mRedfishHttpCachePrivate->CacheDisabled      = PcdGetBool (PcdHttpCacheDisabled);
  InitializeListHead (&mRedfishHttpCachePrivate->CacheList.Head);

  //
  // Get retry settings
  //
  mRedfishHttpCachePrivate->RetrySetting.MaximumRetryGet    = PcdGet16 (PcdHttpGetRetry);
  mRedfishHttpCachePrivate->RetrySetting.MaximumRetryPut    = PcdGet16 (PcdHttpPutRetry);
  mRedfishHttpCachePrivate->RetrySetting.MaximumRetryPatch  = PcdGet16 (PcdHttpPatchRetry);
  mRedfishHttpCachePrivate->RetrySetting.MaximumRetryPost   = PcdGet16 (PcdHttpPostRetry);
  mRedfishHttpCachePrivate->RetrySetting.MaximumRetryDelete = PcdGet16 (PcdHttpDeleteRetry);
  mRedfishHttpCachePrivate->RetrySetting.RetryWait          = PcdGet16 (PcdHttpRetryWaitInSecond) * 1000000U;

  //
  // Install the gEdkIIRedfishHttpProtocolGuid onto Handle.
  //
  Status = gBS->InstallMultipleProtocolInterfaces (
                  &mRedfishHttpCachePrivate->ImageHandle,
                  &gEdkIIRedfishHttpProtocolGuid,
                  &mRedfishHttpCachePrivate->Protocol,
                  NULL
                  );
  if (EFI_ERROR (Status)) {
    DEBUG ((DEBUG_ERROR, "%a: cannot install Redfish http protocol: %r\n", __func__, Status));
    RedfishHttpDriverUnload (ImageHandle);
    return Status;
  }

  //
  // Install protocol notification if credential protocol is installed.
  //
  mRedfishHttpCachePrivate->NotifyEvent = EfiCreateProtocolNotifyEvent (
                                            &gEdkIIRedfishCredential2ProtocolGuid,
                                            TPL_CALLBACK,
                                            CredentialProtocolInstalled,
                                            mRedfishHttpCachePrivate,
                                            &Registration
                                            );
  if (mRedfishHttpCachePrivate->NotifyEvent == NULL) {
    DEBUG ((DEBUG_ERROR, "%a: failed to create protocol notification for gEdkIIRedfishCredential2ProtocolGuid\n", __func__));
    ASSERT (FALSE);
    RedfishHttpDriverUnload (ImageHandle);
    return Status;
  }

  return EFI_SUCCESS;
}
