// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package v1

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/aws/amazon-ecs-agent/ecs-agent/credentials"
	auditinterface "github.com/aws/amazon-ecs-agent/ecs-agent/logger/audit"
	"github.com/aws/amazon-ecs-agent/ecs-agent/logger/audit/request"
	handlersutils "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/handlers/utils"
	"github.com/aws/amazon-ecs-agent/ecs-agent/utils"
	"github.com/cihub/seelog"
)

const (
	// Error Types

	// ErrNoIDInRequest is the error code indicating that no ID was specified
	ErrNoIDInRequest = "NoIdInRequest"

	// ErrInvalidIDInRequest is the error code indicating that the ID was invalid
	ErrInvalidIDInRequest = "InvalidIdInRequest"

	// ErrNoCredentialsAssociated is the error code indicating no credentials are
	// associated with the specified ID
	ErrNoCredentialsAssociated = "NoCredentialsAssociated"

	// ErrCredentialsUninitialized is the error code indicating that credentials were
	// not properly initialized.  This may happen immediately after the agent is
	// started, before it has completed state reconciliation.
	ErrCredentialsUninitialized = "CredentialsUninitialized"

	// ErrInternalServer is the error indicating something generic went wrong
	ErrInternalServer = "InternalServerError"

	// Credentials API version.
	apiVersion = 1

	// CredentialsPath specifies the relative URI path for serving task IAM credentials
	CredentialsPath = credentials.V1CredentialsPath
)

// CredentialsHandler creates response for the 'v1/credentials' API. It returns a JSON response
// containing credentials when found. The HTTP status code of 400 is returned otherwise.
func CredentialsHandler(
	credentialsManager credentials.Manager,
	auditLogger auditinterface.AuditLogger,
) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		credentialsID := getCredentialsID(r)
		errPrefix := fmt.Sprintf("CredentialsV%dRequest: ", apiVersion)
		CredentialsHandlerImpl(w, r, auditLogger, credentialsManager, credentialsID, errPrefix)
	}
}

// CredentialsHandlerImpl is the major logic in CredentialsHandler, abstract this out
// because v2.CredentialsHandler also uses the same logic.
func CredentialsHandlerImpl(
	w http.ResponseWriter,
	r *http.Request,
	auditLogger auditinterface.AuditLogger,
	credentialsManager credentials.Manager,
	credentialsID string,
	errPrefix string,
) {
	responseJSON, arn, roleType, errorMessage, err := processCredentialsRequest(
		credentialsManager, r, credentialsID, errPrefix)
	if err != nil {
		errResponseJSON, err := json.Marshal(errorMessage)
		if e := handlersutils.WriteResponseIfMarshalError(w, err); e != nil {
			return
		}
		writeCredentialsRequestResponse(w, r, errorMessage.HTTPErrorCode,
			auditinterface.GetCredentialsEventTypeFromRoleType(roleType), arn, auditLogger, errResponseJSON)
		return
	}

	writeCredentialsRequestResponse(w, r, http.StatusOK,
		auditinterface.GetCredentialsEventTypeFromRoleType(roleType), arn, auditLogger, responseJSON)
}

// processCredentialsRequest returns the response json containing credentials for the
// credentials id in the request
func processCredentialsRequest(
	credentialsManager credentials.Manager,
	r *http.Request,
	credentialsID string,
	errPrefix string,
) ([]byte, string, string, *handlersutils.ErrorMessage, error) {
	if credentialsID == "" {
		errText := errPrefix + "No Credential ID in the request"
		seelog.Errorf("Error processing credential request: %s", errText)
		msg := &handlersutils.ErrorMessage{
			Code:          ErrNoIDInRequest,
			Message:       errText,
			HTTPErrorCode: http.StatusBadRequest,
		}
		return nil, "", "", msg, errors.New(errText)
	}

	credentials, ok := credentialsManager.GetTaskCredentials(credentialsID)
	if !ok {
		errText := errPrefix + "Credentials not found"
		seelog.Errorf("Error processing credential request: %s", errText)
		msg := &handlersutils.ErrorMessage{
			Code:          ErrInvalidIDInRequest,
			Message:       errText,
			HTTPErrorCode: http.StatusBadRequest,
		}
		return nil, "", "", msg, errors.New(errText)
	}

	seelog.Infof("Processing credential request, credentialType=%s taskARN=%s",
		credentials.IAMRoleCredentials.RoleType, credentials.ARN)

	if utils.ZeroOrNil(credentials.ARN) && utils.ZeroOrNil(credentials.IAMRoleCredentials) {
		// This can happen when the agent is restarted and is reconciling its state.
		errText := errPrefix + "Credentials uninitialized for ID"
		seelog.Errorf("Error processing credential request credentialType=%s taskARN=%s: %s",
			credentials.IAMRoleCredentials.RoleType, credentials.ARN, errText)
		msg := &handlersutils.ErrorMessage{
			Code:          ErrCredentialsUninitialized,
			Message:       errText,
			HTTPErrorCode: http.StatusServiceUnavailable,
		}
		return nil, "", "", msg, errors.New(errText)
	}

	credentialsJSON, err := json.Marshal(credentials.IAMRoleCredentials)
	if err != nil {
		errText := errPrefix + "Error marshaling credentials"
		seelog.Errorf("Error processing credential request credentialType=%s taskARN=%s: %s",
			credentials.IAMRoleCredentials.RoleType, credentials.ARN, errText)
		msg := &handlersutils.ErrorMessage{
			Code:          ErrInternalServer,
			Message:       "Internal server error",
			HTTPErrorCode: http.StatusInternalServerError,
		}
		return nil, "", "", msg, errors.New(errText)
	}

	// Success
	return credentialsJSON, credentials.ARN, credentials.IAMRoleCredentials.RoleType, nil, nil
}

func writeCredentialsRequestResponse(
	w http.ResponseWriter,
	r *http.Request,
	httpStatusCode int,
	eventType string,
	arn string,
	auditLogger auditinterface.AuditLogger,
	message []byte,
) {
	auditLogger.Log(request.LogRequest{Request: r, ARN: arn}, httpStatusCode, eventType)
	handlersutils.WriteJSONToResponse(w, httpStatusCode, message, handlersutils.RequestTypeCreds)
}

func getCredentialsID(r *http.Request) string {
	credentialsID, ok := handlersutils.ValueFromRequest(r, credentials.CredentialsIDQueryParameterName)
	if ok {
		return credentialsID
	}
	return ""
}
