// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package logging

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"io"
	"net/http"
	"net/http/httputil"
	"net/textproto"
	"strings"

	"github.com/hashicorp/go-uuid"
	"github.com/hashicorp/terraform-plugin-log/tflog"
)

// NewLoggingHTTPTransport creates a wrapper around an *http.RoundTripper,
// designed to be used for the `Transport` field of http.Client.
//
// This logs each pair of HTTP request/response that it handles.
// The logging is done via `tflog`, that is part of the terraform-plugin-log
// library, included by this SDK.
//
// The request/response is logged via tflog.Debug, using the context.Context
// attached to the http.Request that the transport receives as input
// of http.RoundTripper RoundTrip method.
//
// It's responsibility of the developer using this transport, to ensure that each
// http.Request it handles is configured with the SDK-initialized Provider Root Logger
// context.Context, that it's passed to all resources/data-sources/provider entry-points
// (i.e. schema.Resource fields like `CreateContext`, `ReadContext`, etc.).
//
// This also gives the developer the flexibility to further configure the
// logging behaviour via the above-mentioned context: please see
// https://www.terraform.io/plugin/log/writing.
func NewLoggingHTTPTransport(t http.RoundTripper) *loggingHttpTransport {
	return &loggingHttpTransport{"", t}
}

// NewSubsystemLoggingHTTPTransport creates a wrapper around an *http.RoundTripper,
// designed to be used for the `Transport` field of http.Client.
//
// This logs each pair of HTTP request/response that it handles.
// The logging is done via `tflog`, that is part of the terraform-plugin-log
// library, included by this SDK.
//
// The request/response is logged via tflog.SubsystemDebug, using the context.Context
// attached to the http.Request that the transport receives as input
// of http.RoundTripper RoundTrip method, as well as the `subsystem` string
// provided at construction time.
//
// It's responsibility of the developer using this transport, to ensure that each
// http.Request it handles is configured with a Subsystem Logger
// context.Context that was initialized via tflog.NewSubsystem.
//
// This also gives the developer the flexibility to further configure the
// logging behaviour via the above-mentioned context: please see
// https://www.terraform.io/plugin/log/writing.
//
// Please note: setting `subsystem` to an empty string it's equivalent to
// using NewLoggingHTTPTransport.
func NewSubsystemLoggingHTTPTransport(subsystem string, t http.RoundTripper) *loggingHttpTransport {
	return &loggingHttpTransport{subsystem, t}
}

const (
	// FieldHttpOperationType is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging the type of HTTP operation via tflog.
	FieldHttpOperationType = "tf_http_op_type"

	// OperationHttpRequest is the field value used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP request via tflog.
	OperationHttpRequest = "request"

	// OperationHttpResponse is the field value used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP response via tflog.
	OperationHttpResponse = "response"

	// FieldHttpRequestMethod is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP request method via tflog.
	FieldHttpRequestMethod = "tf_http_req_method"

	// FieldHttpRequestUri is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP request URI via tflog.
	FieldHttpRequestUri = "tf_http_req_uri"

	// FieldHttpRequestProtoVersion is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP request HTTP version via tflog.
	FieldHttpRequestProtoVersion = "tf_http_req_version"

	// FieldHttpRequestBody is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP request body via tflog.
	FieldHttpRequestBody = "tf_http_req_body"

	// FieldHttpResponseProtoVersion is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP response protocol version via tflog.
	FieldHttpResponseProtoVersion = "tf_http_res_version"

	// FieldHttpResponseStatusCode is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP response status code via tflog.
	FieldHttpResponseStatusCode = "tf_http_res_status_code"

	// FieldHttpResponseStatusReason is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP response status reason phrase via tflog.
	FieldHttpResponseStatusReason = "tf_http_res_status_reason"

	// FieldHttpResponseBody is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP response body via tflog.
	FieldHttpResponseBody = "tf_http_res_body"

	// FieldHttpTransactionId is the field key used by NewLoggingHTTPTransport
	// and NewSubsystemLoggingHTTPTransport when logging an HTTP transaction via tflog.
	FieldHttpTransactionId = "tf_http_trans_id"
)

type loggingHttpTransport struct {
	subsystem string
	transport http.RoundTripper
}

func (t *loggingHttpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	ctx := req.Context()
	ctx = t.AddTransactionIdField(ctx)

	// Decompose the request bytes in a message (HTTP body) and fields (HTTP headers), then log it
	fields, err := decomposeRequestForLogging(req)
	if err != nil {
		t.Error(ctx, "Failed to parse request bytes for logging", map[string]interface{}{
			"error": err,
		})
	} else {
		t.Debug(ctx, "Sending HTTP Request", fields)
	}

	// Invoke the wrapped RoundTrip now
	res, err := t.transport.RoundTrip(req)
	if err != nil {
		return res, err
	}

	// Decompose the response bytes in a message (HTTP body) and fields (HTTP headers), then log it
	fields, err = decomposeResponseForLogging(res)
	if err != nil {
		t.Error(ctx, "Failed to parse response bytes for logging", map[string]interface{}{
			"error": err,
		})
	} else {
		t.Debug(ctx, "Received HTTP Response", fields)
	}

	return res, nil
}

func (t *loggingHttpTransport) Debug(ctx context.Context, msg string, fields ...map[string]interface{}) {
	if t.subsystem != "" {
		tflog.SubsystemDebug(ctx, t.subsystem, msg, fields...)
	} else {
		tflog.Debug(ctx, msg, fields...)
	}
}

func (t *loggingHttpTransport) Error(ctx context.Context, msg string, fields ...map[string]interface{}) {
	if t.subsystem != "" {
		tflog.SubsystemError(ctx, t.subsystem, msg, fields...)
	} else {
		tflog.Error(ctx, msg, fields...)
	}
}

func (t *loggingHttpTransport) AddTransactionIdField(ctx context.Context) context.Context {
	tId, err := uuid.GenerateUUID()

	if err != nil {
		tId = "Unable to assign Transaction ID: " + err.Error()
	}

	if t.subsystem != "" {
		return tflog.SubsystemSetField(ctx, t.subsystem, FieldHttpTransactionId, tId)
	} else {
		return tflog.SetField(ctx, FieldHttpTransactionId, tId)

	}
}

func decomposeRequestForLogging(req *http.Request) (map[string]interface{}, error) {
	fields := make(map[string]interface{}, len(req.Header)+4)
	fields[FieldHttpOperationType] = OperationHttpRequest

	fields[FieldHttpRequestMethod] = req.Method
	fields[FieldHttpRequestUri] = req.URL.RequestURI()
	fields[FieldHttpRequestProtoVersion] = req.Proto

	// Get the full body of the request, including headers appended by http.Transport:
	// this is necessary because the http.Request at this stage doesn't contain
	// all the headers that will be eventually sent.
	// We rely on `httputil.DumpRequestOut` to obtain the actual bytes that will be sent out.
	reqBytes, err := httputil.DumpRequestOut(req, true)
	if err != nil {
		return nil, err
	}

	// Create a reader around the request full body
	reqReader := textproto.NewReader(bufio.NewReader(bytes.NewReader(reqBytes)))

	err = fieldHeadersFromRequestReader(reqReader, fields)
	if err != nil {
		return nil, err
	}

	// Read the rest of the body content
	fields[FieldHttpRequestBody] = bodyFromRestOfRequestReader(reqReader)
	return fields, nil
}

func fieldHeadersFromRequestReader(reader *textproto.Reader, fields map[string]interface{}) error {
	// Ignore the first line: it contains non-header content
	// that we have already captured.
	// Skipping this step, would cause the following call to `ReadMIMEHeader()`
	// to fail as it cannot parse the first line.
	_, err := reader.ReadLine()
	if err != nil {
		return err
	}

	// Read the MIME-style headers
	mimeHeader, err := reader.ReadMIMEHeader()
	if err != nil {
		return err
	}

	// Set the headers as fields to log
	for k, v := range mimeHeader {
		if len(v) == 1 {
			fields[k] = v[0]
		} else {
			fields[k] = v
		}
	}

	return nil
}

func bodyFromRestOfRequestReader(reader *textproto.Reader) string {
	var builder strings.Builder
	for {
		line, err := reader.ReadContinuedLine()
		if errors.Is(err, io.EOF) {
			break
		}
		builder.WriteString(line)
	}

	return builder.String()
}

func decomposeResponseForLogging(res *http.Response) (map[string]interface{}, error) {
	fields := make(map[string]interface{}, len(res.Header)+4)
	fields[FieldHttpOperationType] = OperationHttpResponse

	fields[FieldHttpResponseProtoVersion] = res.Proto
	fields[FieldHttpResponseStatusCode] = res.StatusCode
	fields[FieldHttpResponseStatusReason] = res.Status

	// Set the headers as fields to log
	for k, v := range res.Header {
		if len(v) == 1 {
			fields[k] = v[0]
		} else {
			fields[k] = v
		}
	}

	// Read the whole response body
	resBody, err := io.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	// Wrap the bytes from the response body, back into an io.ReadCloser,
	// to respect the interface of http.Response, as expected by users of the
	// http.Client
	res.Body = io.NopCloser(bytes.NewBuffer(resBody))

	fields[FieldHttpResponseBody] = string(resBody)

	return fields, nil
}
