// Copyright 2018-2024 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package response

import (
	"bytes"
	"context"
	"encoding/json"
	"encoding/xml"
	"net/http"
	"reflect"

	"github.com/cs3org/reva/v3/pkg/appctx"
	"github.com/cs3org/reva/v3/pkg/trace"
	"github.com/go-chi/chi/v5"
)

type key int

const (
	apiVersionKey key = 1
)

var defaultStatusCodeMapper = OcsV2StatusCodes

// Response is the top level response structure.
type Response struct {
	OCS *Payload `json:"ocs"`
}

// Payload combines response metadata and data.
type Payload struct {
	XMLName struct{} `json:"-"              xml:"ocs"`
	Meta    Meta     `json:"meta"           xml:"meta"`
	Data    any      `json:"data,omitempty" xml:"data,omitempty"`
}

var (
	elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
	metaStartElement    = xml.StartElement{Name: xml.Name{Local: "meta"}}
	ocsName             = xml.Name{Local: "ocs"}
	dataName            = xml.Name{Local: "data"}
)

// MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data.
func (p Payload) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
	// first the easy part
	// use ocs as the surrounding tag
	start.Name = ocsName
	if err = e.EncodeToken(start); err != nil {
		return
	}

	// encode the meta tag
	if err = e.EncodeElement(p.Meta, metaStartElement); err != nil {
		return
	}

	// we need to use reflection to determine if p.Data is an array or a slice
	rt := reflect.TypeOf(p.Data)
	if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
		// this is how to wrap the data elements in their own <element> tag
		v := reflect.ValueOf(p.Data)
		if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
			return
		}
		for i := 0; i < v.Len(); i++ {
			if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
				return
			}
		}
		if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
			return
		}
	} else if err = e.EncodeElement(p.Data, xml.StartElement{Name: dataName}); err != nil {
		return
	}

	// write the closing <ocs> tag
	if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
		return
	}
	return
}

// Meta holds response metadata.
type Meta struct {
	Status       string `json:"status"                 xml:"status"`
	StatusCode   int    `json:"statuscode"             xml:"statuscode"`
	Message      string `json:"message"                xml:"message"`
	TotalItems   string `json:"totalitems,omitempty"   xml:"totalitems,omitempty"`
	ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"`
}

// MetaOK is the default ok response.
var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"}

// MetaBadRequest is used for unknown errors.
var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"}

// MetaServerError is returned on server errors.
var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"}

// MetaUnauthorized is returned on unauthorized requests.
var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"}

// MetaNotFound is returned when trying to access not existing resources.
var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"}

// MetaUnknownError is used for unknown errors.
var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"}

// WriteOCSSuccess handles writing successful ocs response data.
func WriteOCSSuccess(w http.ResponseWriter, r *http.Request, d any) {
	WriteOCSData(w, r, MetaOK, d, nil)
}

// WriteOCSError handles writing error ocs responses.
func WriteOCSError(w http.ResponseWriter, r *http.Request, c int, m string, err error) {
	WriteOCSData(w, r, Meta{Status: "error", StatusCode: c, Message: m}, nil, err)
}

// WriteOCSData handles writing ocs data in json and xml.
func WriteOCSData(w http.ResponseWriter, r *http.Request, m Meta, d any, err error) {
	ctx := r.Context()
	w.Header().Set("x-request-id", trace.Get(ctx))
	WriteOCSResponse(w, r, Response{
		OCS: &Payload{
			Meta: m,
			Data: d,
		},
	}, err)
}

// WriteOCSResponse handles writing ocs responses in json and xml.
func WriteOCSResponse(w http.ResponseWriter, r *http.Request, res Response, err error) {
	if err != nil {
		appctx.GetLogger(r.Context()).Error().Err(err).Msg(res.OCS.Meta.Message)
	}

	version := APIVersion(r.Context())
	m := statusCodeMapper(version)
	statusCode := m(res.OCS.Meta)
	if version == "2" && statusCode == http.StatusOK {
		res.OCS.Meta.StatusCode = statusCode
	}

	var encoder func(Response) ([]byte, error)
	if r.URL.Query().Get("format") == "json" {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		encoder = encodeJSON
	} else {
		w.Header().Set("Content-Type", "text/xml; charset=utf-8")
		encoder = encodeXML
	}
	w.WriteHeader(statusCode)
	encoded, err := encoder(res)
	if err != nil {
		appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding ocs response")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	_, err = w.Write(encoded)
	if err != nil {
		appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing ocs response")
		w.WriteHeader(http.StatusInternalServerError)
	}
}

func encodeXML(res Response) ([]byte, error) {
	marshalled, err := xml.Marshal(res.OCS)
	if err != nil {
		return nil, err
	}
	b := new(bytes.Buffer)
	b.WriteString(xml.Header)
	b.Write(marshalled)
	return b.Bytes(), nil
}

func encodeJSON(res Response) ([]byte, error) {
	return json.Marshal(res)
}

// OcsV1StatusCodes returns the http status codes for the OCS API v1.
func OcsV1StatusCodes(meta Meta) int {
	return http.StatusOK
}

// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
func OcsV2StatusCodes(meta Meta) int {
	sc := meta.StatusCode
	switch sc {
	case MetaNotFound.StatusCode:
		return http.StatusNotFound
	case MetaUnknownError.StatusCode:
		fallthrough
	case MetaServerError.StatusCode:
		return http.StatusInternalServerError
	case MetaUnauthorized.StatusCode:
		return http.StatusUnauthorized
	case 100:
		meta.StatusCode = http.StatusOK
		return http.StatusOK
	}
	// any 2xx, 4xx and 5xx will be used as is
	if sc >= 200 && sc < 600 {
		return sc
	}

	// any error codes > 100 are treated as client errors
	if sc > 100 && sc < 200 {
		return http.StatusBadRequest
	}

	// TODO change this status code?
	return http.StatusOK
}

// WithAPIVersion puts the api version in the context.
func VersionCtx(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		version := chi.URLParam(r, "version")
		if version == "" {
			WriteOCSError(w, r, MetaBadRequest.StatusCode, "unknown ocs api version", nil)
			return
		}
		w.Header().Set("Ocs-Api-Version", version)

		// store version in context so handlers can access it
		ctx := context.WithValue(r.Context(), apiVersionKey, version)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// APIVersion retrieves the api version from the context.
func APIVersion(ctx context.Context) string {
	value := ctx.Value(apiVersionKey)
	if value != nil {
		return value.(string)
	}
	return ""
}

func statusCodeMapper(version string) func(Meta) int {
	var mapper func(Meta) int
	switch version {
	case "1":
		mapper = OcsV1StatusCodes
	case "2":
		mapper = OcsV2StatusCodes
	default:
		mapper = defaultStatusCodeMapper
	}
	return mapper
}
