/*
 * Copyright (C) 2023-2024
 * Das Land Schleswig-Holstein vertreten durch den
 * Ministerpräsidenten des Landes Schleswig-Holstein
 * Staatskanzlei
 * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
 *
 * Lizenziert unter der EUPL, Version 1.2 oder - sobald
 * diese von der Europäischen Kommission genehmigt wurden -
 * Folgeversionen der EUPL ("Lizenz");
 * Sie dürfen dieses Werk ausschließlich gemäß
 * dieser Lizenz nutzen.
 * Eine Kopie der Lizenz finden Sie hier:
 *
 * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
 *
 * Sofern nicht durch anwendbare Rechtsvorschriften
 * gefordert oder in schriftlicher Form vereinbart, wird
 * die unter der Lizenz verbreitete Software "so wie sie
 * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
 * ausdrücklich oder stillschweigend - verbreitet.
 * Die sprachspezifischen Genehmigungen und Beschränkungen
 * unter der Lizenz sind dem Lizenztext zu entnehmen.
 */

package server

import (
	pb "antragsraum-proxy/gen/go"
	"bytes"
	"context"
	"fmt"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/encoding/protojson"
	"io"
	"mime/multipart"
	"net/http"
)

const (
	fileFormKey          = "file"
	vorgangIdFormKey     = "vorgangId"
	contentTypeHeaderKey = "Content-Type"
	maxFileSizeInMB      = 150
	fileChunkSizeInByte  = 1024 * 1024
)



func RegisterHomeEndpoint(mux *runtime.ServeMux) {
	err := mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
		defer func() {
			if err := recover(); err != nil {
				log.WithError(err).Error("failed to recover: %v", err)
				http.Error(w, fmt.Sprintf("failed to recover: %v", err), http.StatusInternalServerError)
			}
		}()
	})

	if err != nil {
	    log.WithError(err).Error("failed to register home endpoint: %v", err)
	}
}

func RegisterAntragraumEndpoints(ctx context.Context, mux *runtime.ServeMux, grpcUrl string) {
	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	err := pb.RegisterAntragraumServiceHandlerFromEndpoint(ctx, mux, grpcUrl, opts)
	if err != nil {
	    log.WithError(err).Error("failed to register antragraum endpoint: %v", err)
	}
}

func RegisterCommandEndpoints(ctx context.Context, mux *runtime.ServeMux, grpcUrl string) {
	opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
	err := pb.RegisterCommandServiceHandlerFromEndpoint(ctx, mux, grpcUrl, opts)
	if err != nil {
	    log.WithError(err).Error("failed to register command endpoint: %v", err)

	}
}

func RegisterUploadAttachmentEndpoint(mux *runtime.ServeMux) {
	err := mux.HandlePath("POST", "/api/v1/file", attachmentUploadHandler)

	if err != nil {
		log.WithError(err).Error("failed to register upload endpoint: %v", err)
	}
}

func attachmentUploadHandler(w http.ResponseWriter, r *http.Request, _ map[string]string) {
	if err := r.ParseMultipartForm(maxFileSizeInMB << 20); err != nil {
		http.Error(w, "invalid multipart form", http.StatusBadRequest)
		return
	}

	file, header, err := r.FormFile(fileFormKey)
	if err != nil {
		http.Error(w, "file retrieval error", http.StatusBadRequest)
		return
	}
	defer file.Close()

	client, stream, err := createGrpcStream(r)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer client.Close()

	if err := sendFileMetadata(stream, r, header); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	if err := sendFileChunks(stream, file); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	resp, err := stream.CloseAndRecv()
	if err != nil {
		http.Error(w, "response error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusOK)
	w.Write([]byte(resp.FileId))
}

func createGrpcStream(r *http.Request) (*grpc.ClientConn, pb.BinaryFileService_UploadBinaryFileAsStreamClient, error) {
	target := GetGrpcServerUrl(r.Header.Get(GrpcAddressHeader), conf)
	conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, nil, fmt.Errorf("gRPC connection error")
	}

	client := pb.NewBinaryFileServiceClient(conn)
	stream, err := client.UploadBinaryFileAsStream(r.Context())
	if err != nil {
		return nil, nil, fmt.Errorf("stream creation error")
	}

	return conn, stream, nil
}

func sendFileMetadata(stream pb.BinaryFileService_UploadBinaryFileAsStreamClient, r *http.Request, header *multipart.FileHeader) error {
	meta := &pb.GrpcUploadBinaryFileRequest{
		Request: &pb.GrpcUploadBinaryFileRequest_Metadata{
			Metadata: &pb.GrpcUploadBinaryFileMetaData{
				VorgangId:   r.FormValue(vorgangIdFormKey),
				FileName:    header.Filename,
				ContentType: header.Header.Get(contentTypeHeaderKey),
			},
		},
	}

	if err := stream.Send(meta); err != nil {
		return fmt.Errorf("metadata send error")
	}

	return nil
}

func sendFileChunks(stream pb.BinaryFileService_UploadBinaryFileAsStreamClient, file multipart.File) error {
	buf := make([]byte, fileChunkSizeInByte)
	for {
		n, err := file.Read(buf)
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("file read error")
		}

		req := &pb.GrpcUploadBinaryFileRequest{
			Request: &pb.GrpcUploadBinaryFileRequest_FileContent{
				FileContent: buf[:n],
			},
		}
		if err = stream.Send(req); err != nil {
			return fmt.Errorf("chunk send error")
		}
	}

	return nil
}

func RegisterGetAttachmentContentEndpoint(mux *runtime.ServeMux) {
	err := mux.HandlePath("POST", "/api/v1/file/content", getAttachmentContentHandler)

	if err != nil {
		log.WithError(err).Error("failed to register get file endpoint: %v", err)

	}
}

func getAttachmentContentHandler(w http.ResponseWriter, r *http.Request, _ map[string]string) {
	requestPayload, err := getGetAttachmentContentRequestPayload(r)
	if err != nil {
		http.Error(w, "failed to read request body", http.StatusBadRequest)
	}

	fileBuffer, err := fetchFileFromGrpc(r.Context(), requestPayload, r.Header.Get(GrpcAddressHeader))
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set(contentTypeHeaderKey, "application/octet-stream")
	w.WriteHeader(http.StatusOK)

	_, err = fileBuffer.WriteTo(w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func getGetAttachmentContentRequestPayload(r *http.Request) (*pb.GrpcGetAttachmentContentRequest, error) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		return nil, err
	}
	defer r.Body.Close()

	var requestPayload pb.GrpcGetAttachmentContentRequest
	if err = protojson.Unmarshal(body, &requestPayload); err != nil {
		return nil, err
	}

	return &requestPayload, nil
}

func fetchFileFromGrpc(ctx context.Context, request *pb.GrpcGetAttachmentContentRequest, grpcAddress string) (*bytes.Buffer, error) {
	var fileBuffer bytes.Buffer

	target := GetGrpcServerUrl(grpcAddress, conf)
	conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, fmt.Errorf("gRPC connection error")
	}
	defer conn.Close()

	client := pb.NewAntragraumServiceClient(conn)
	clientStream, err := client.GetAttachmentContent(ctx, request)
	if err != nil {
		return nil, fmt.Errorf("stream creation error")
	}

	for {
		resp, err := clientStream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("error receiving file chunks")
		}

		_, err = fileBuffer.Write(resp.FileContent)
		if err != nil {
			return nil, fmt.Errorf("error writing file data")
		}
	}

	return &fileBuffer, nil
}

func ErrorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
	st, ok := status.FromError(err)
	if !ok {
		runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, err)
		return
	}

	statusCodeMap := map[codes.Code]int{
		codes.InvalidArgument: http.StatusBadRequest,
		codes.AlreadyExists:   http.StatusConflict,
		codes.Unavailable:     http.StatusServiceUnavailable,
	}

	httpStatusCode := http.StatusInternalServerError

	if code, exists := statusCodeMap[st.Code()]; exists {
		httpStatusCode = code
	}

	w.Header().Set("Content-Type", marshaler.ContentType(r))
	w.WriteHeader(httpStatusCode)

	_, writeErr := w.Write([]byte(st.Message()))
	if writeErr != nil {
		log.WithError(err).Error("failed to handle grpc error: %v", writeErr)
	}
}