package api

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	telemetry "code.justin.tv/amzn/TwitchTelemetry"
	"code.justin.tv/common/twitchhttp"
	"code.justin.tv/creator-collab/log"
	autohosterrors "code.justin.tv/live/autohost/internal/errors"
	"code.justin.tv/live/autohost/internal/hosting"
	hosting_logic "code.justin.tv/live/autohost/internal/hosting/logic"
	"code.justin.tv/live/autohost/internal/logging"
	"code.justin.tv/live/autohost/internal/metrics"
	hosting_rpc "code.justin.tv/live/autohost/rpc/hosting"
	"github.com/zenazn/goji/web/mutil"
	"goji.io"
	"goji.io/pat"
)

const (
	// requestTimeout is the amount of time we wait for a request to process before
	// cancelling the request context.
	requestTimeout = 20 * time.Second
)

// Server is an http.Server that uses an underlying Logic
type Server struct {
	*goji.Mux

	hostingLogic   hosting_logic.Logic
	sampleReporter *telemetry.SampleReporter
	errorLogger    logging.ErrorLogger
	logger         log.Logger
}

// NewServer creates a new server
func NewServer(hostingComponents *hosting.Components) (*Server, error) {
	s := &Server{
		Mux:            twitchhttp.NewServer(),
		hostingLogic:   hostingComponents.Logic,
		sampleReporter: hostingComponents.SampleReporter,
		logger:         hostingComponents.Logger,
		errorLogger: &logging.CollabLogErrorLogger{
			Logger: hostingComponents.Logger,
		},
	}

	s.HandleFuncC(pat.Get("/settings/:id"), s.createHandler(s.getSettings, "GetSettings"))
	s.HandleFuncC(pat.Put("/settings/:id"), s.createHandler(s.updateSettings, "UpdateSettings"))
	s.HandleFuncC(pat.Post("/host"), s.createHandler(s.hostTarget, "HostTarget"))
	s.HandleFuncC(pat.Delete("/host"), s.createHandler(s.unhost, "Unhost"))

	// Twitch Telemetry twirp hooks.
	telemetryTwirpMiddleware := metrics.NewTwirpServerMiddleware(*hostingComponents.SampleReporter)
	hooks := telemetryTwirpMiddleware.ServerHooks()

	// Register Twirp RPC endpoints.
	hostingTwirpHandler := hosting_rpc.NewHostingServer(hostingComponents.TwirpHandlers, hooks)
	hostingWrappedHandler := http.TimeoutHandler(hostingTwirpHandler, requestTimeout, "request timed out")
	s.Handle(pat.Post(hosting_rpc.HostingPathPrefix+"*"), hostingWrappedHandler)

	return s, nil
}

// createHandler creates a handler that posts timing and response code metrics to CloudWatch.
func (S *Server) createHandler(
	handler func(context.Context, http.ResponseWriter, *http.Request),
	operationName string) func(context.Context, http.ResponseWriter, *http.Request) {

	// This will be value of the "Operation" dimension for all metrics related to this handler.
	// We preface it with 'api' to avoid a naming conflict with metrics from the Twirp APIs.
	fullOperationName := fmt.Sprintf("api/%s", operationName)

	// Create a sample reporter that we will use to send metrics on the given handler.
	handlerSampleReporter := *S.sampleReporter
	handlerSampleReporter.OperationName = fullOperationName

	return func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
		wrappedWriter := mutil.WrapWriter(w)
		start := time.Now()

		// Attach the Operation to the context. This allows Twitch Telemetry to include the
		// Operation in metrics on the handler's dependencies.
		ctx = telemetry.ContextWithOperationName(ctx, fullOperationName)

		// Handle the request.
		handler(ctx, wrappedWriter, r)

		// Report stats to Twitch Telemetry (which reports to CloudWatch).
		duration := time.Since(start)
		statusCode := wrappedWriter.Status()
		availabilityCode := telemetry.AvailabilityCodeSucccess
		if statusCode >= 500 {
			availabilityCode = telemetry.AvailabilityCodeServerError
		} else if statusCode >= 400 {
			availabilityCode = telemetry.AvailabilityCodeClientError
		}

		handlerSampleReporter.ReportAvailabilitySamples(availabilityCode)
		handlerSampleReporter.ReportDurationSample(telemetry.MetricDuration, duration)
	}
}

func (S *Server) serveJSON(w http.ResponseWriter, v interface{}) {
	S.serve(w, v, http.StatusOK)
}

func (S *Server) serveError(w http.ResponseWriter, r *http.Request, err error) {
	status := http.StatusInternalServerError
	rollbarErr := err
	if httperr, ok := err.(autohosterrors.HTTPError); ok {
		status = httperr.Code
	} else {
		err = autohosterrors.InternalServerError(err)
	}

	S.serve(w, err, status)

	if S.errorLogger != nil && status >= 500 {
		S.errorLogger.RequestError(r, rollbarErr)
	}
}

func (S *Server) serve(w http.ResponseWriter, v interface{}, status int) {
	content, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Length", strconv.Itoa(len(content)))
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_, err = w.Write(content)
	if err != nil {
		S.errorLogger.Error(err)
	}
}

func getUserIDFromRequestPath(requestCtx context.Context) (string, error) {
	userID := pat.Param(requestCtx, "id")

	err := validateUserID(":id", userID)
	if err != nil {
		return "", err
	}

	return userID, nil
}

func parseBool(value string) *bool {
	b, err := strconv.ParseBool(value)
	if err != nil {
		return nil
	}

	return &b
}
