package dialectsfcrm

import (
	"context"
	"crypto/rand"
	"fmt"
	"io"
	"mime"
	"mime/multipart"
	"net/http"
	"path"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/nexuscrm/dialect-sf-crm/wire"
	"github.com/nexuscrm/shared/pkg/contract"
	"github.com/nexuscrm/shared/pkg/dialect"
	sharederrors "github.com/nexuscrm/shared/pkg/errors"
)

const (
	uploadFieldParentID            = "parentId"
	uploadFieldParentObjectAPIName = "parentObjectApiName"
	uploadFieldAttachmentKind      = "attachmentKind"
	defaultRecordAttachmentKind    = "record"
	maxV63FileUploadSizeBytes      = 25 * 1024 * 1024
)

var allowedFileExtensions = map[string]bool{
	".pdf": true, ".doc": true, ".docx": true,
	".xls": true, ".xlsx": true, ".ppt": true, ".pptx": true,
	".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
	".csv": true, ".txt": true, ".json": true, ".xml": true,
	".zip": true,
}

var blockedFileMIMEs = map[string]bool{
	"text/html":              true,
	"application/javascript": true,
	"application/xhtml+xml":  true,
	"image/svg+xml":          true,
}

var legacyContentVersionUUIDStem = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)

func UploadLegacyContentVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return handleFileUpload(ctx, req, deps, false)
}

func UploadRecordFileNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	return handleFileUpload(ctx, req, deps, true)
}

func handleFileUpload(ctx context.Context, req *dialect.Request, deps dialect.Deps, requireParentContext bool) (*dialect.Response, error) {
	files := deps.Contracts.Files
	if files == nil {
		return internalErrorResponse("file service unavailable"), nil
	}

	upload, resp := readMultipartFile(req)
	if resp != nil {
		return resp, nil
	}
	defer upload.Close()

	if upload.Size > maxV63FileUploadSizeBytes {
		return fileTooLargeResponse(), nil
	}

	ext := strings.ToLower(filepath.Ext(upload.FileName))
	if !allowedFileExtensions[ext] {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, fmt.Sprintf("File type '%s' not allowed", ext)),
		}, nil
	}

	detectedMime, err := sniffMultipartFile(upload.File)
	if err != nil {
		return internalErrorResponse("Failed to process file"), nil
	}
	if blockedFileMIMEs[normalizeFileMediaType(detectedMime)] {
		return &dialect.Response{
			Status: http.StatusBadRequest,
			Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, "File content type not allowed"),
		}, nil
	}

	parentID := strings.TrimSpace(upload.FormValue(uploadFieldParentID))
	parentObjectAPIName := strings.TrimSpace(upload.FormValue(uploadFieldParentObjectAPIName))
	attachmentKind := strings.TrimSpace(upload.FormValue(uploadFieldAttachmentKind))
	if attachmentKind == "" {
		attachmentKind = defaultRecordAttachmentKind
	}

	if requireParentContext || parentID != "" || parentObjectAPIName != "" {
		if parentID == "" || parentObjectAPIName == "" {
			return &dialect.Response{
				Status: http.StatusBadRequest,
				Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, "parentId and parentObjectApiName are required"),
			}, nil
		}
		result, err := files.StoreRecordAttachment(ctx, contract.FileUploadRequest{
			FileName:            upload.FileName,
			Extension:           ext,
			ContentType:         detectedMime,
			Body:                upload.File,
			ContentLength:       upload.Size,
			ParentID:            parentID,
			ParentObjectAPIName: parentObjectAPIName,
			AttachmentKind:      attachmentKind,
			User:                req.User,
		})
		if err != nil {
			return fileOperationErrorResponse(req, err, "Failed to upload record file"), nil
		}
		return &dialect.Response{Status: http.StatusCreated, Body: map[string]any{"data": fileUploadWireResult(req.Version, result, true)}}, nil
	}

	fileID, err := newLegacyContentVersionID(ext)
	if err != nil {
		return internalErrorResponse("Failed to store file"), nil
	}
	result, err := files.StoreDetachedFile(ctx, contract.FileUploadRequest{
		ID:             fileID,
		StorageKey:     legacyContentVersionStorageKey(fileID),
		LegacyFileName: fileID,
		FileName:       upload.FileName,
		Extension:      ext,
		ContentType:    detectedMime,
		Body:           upload.File,
		ContentLength:  upload.Size,
		User:           req.User,
	})
	if err != nil {
		return fileOperationErrorResponse(req, err, "Failed to store file"), nil
	}
	return &dialect.Response{Status: http.StatusOK, Body: map[string]any{"data": fileUploadWireResult(req.Version, result, false)}}, nil
}

func DownloadLegacyContentVersionNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	files := deps.Contracts.Files
	if files == nil {
		return internalErrorResponse("file service unavailable"), nil
	}
	fileID := strings.TrimSpace(req.Params["fileId"])
	if download, found, err := files.OpenRecordAttachment(ctx, fileID, req.User); err != nil {
		return fileDownloadErrorResponse(req, err), nil
	} else if found {
		return fileDownloadResponse(download), nil
	}
	if resp := validateLegacyContentVersionID(fileID); resp != nil {
		return resp, nil
	}
	download, err := files.OpenDetachedFile(ctx, legacyContentVersionBlobReference(fileID), req.User)
	if err != nil {
		return fileDownloadErrorResponse(req, err), nil
	}
	return fileDownloadResponse(download), nil
}

func DownloadRecordFileNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	files := deps.Contracts.Files
	if files == nil {
		return internalErrorResponse("file service unavailable"), nil
	}
	download, found, err := files.OpenRecordAttachment(ctx, strings.TrimSpace(req.Params["id"]), req.User)
	if err != nil {
		return fileDownloadErrorResponse(req, err), nil
	}
	if !found {
		return notFoundResponse("File not found"), nil
	}
	return fileDownloadResponse(download), nil
}

func DeleteRecordFileNeutralHandler(ctx context.Context, req *dialect.Request, deps dialect.Deps) (*dialect.Response, error) {
	files := deps.Contracts.Files
	if files == nil {
		return internalErrorResponse("file service unavailable"), nil
	}
	if err := files.DeleteRecordAttachment(ctx, strings.TrimSpace(req.Params["id"]), req.User); err != nil {
		return fileOperationErrorResponse(req, err, "Failed to delete file"), nil
	}
	return &dialect.Response{Status: http.StatusNoContent}, nil
}

type multipartUpload struct {
	form     *multipart.Form
	File     multipart.File
	FileName string
	Size     int64
}

func (u *multipartUpload) Close() {
	if u == nil {
		return
	}
	if u.File != nil {
		_ = u.File.Close()
	}
	if u.form != nil {
		_ = u.form.RemoveAll()
	}
}

func (u *multipartUpload) FormValue(name string) string {
	if u == nil || u.form == nil {
		return ""
	}
	values := u.form.Value[name]
	if len(values) == 0 {
		return ""
	}
	return values[0]
}

func readMultipartFile(req *dialect.Request) (*multipartUpload, *dialect.Response) {
	if req == nil || req.BodyStream == nil {
		return nil, noFileProvidedResponse()
	}
	contentType := firstFileHeader(req.Header, "Content-Type")
	mediaType, params, err := mime.ParseMediaType(contentType)
	if err != nil || !strings.HasPrefix(strings.ToLower(mediaType), "multipart/") || strings.TrimSpace(params["boundary"]) == "" {
		return nil, noFileProvidedResponse()
	}

	reader := multipart.NewReader(req.BodyStream, params["boundary"])
	form, err := reader.ReadForm(maxV63FileUploadSizeBytes)
	if err != nil {
		if dialect.IsBodyStreamTooLarge(err) {
			return nil, fileTooLargeResponse()
		}
		return nil, noFileProvidedResponse()
	}
	fileHeaders := form.File["file"]
	if len(fileHeaders) == 0 || fileHeaders[0] == nil {
		_ = form.RemoveAll()
		return nil, noFileProvidedResponse()
	}
	file, err := fileHeaders[0].Open()
	if err != nil {
		_ = form.RemoveAll()
		return nil, internalErrorResponse("Failed to read file")
	}
	return &multipartUpload{
		form:     form,
		File:     file,
		FileName: fileHeaders[0].Filename,
		Size:     fileHeaders[0].Size,
	}, nil
}

func sniffMultipartFile(file multipart.File) (string, error) {
	sniffBuf := make([]byte, 512)
	n, _ := file.Read(sniffBuf)
	detectedMime := http.DetectContentType(sniffBuf[:n])
	if _, err := file.Seek(0, io.SeekStart); err != nil {
		return "", err
	}
	return detectedMime, nil
}

func fileUploadWireResult(version string, result *contract.FileUploadResult, recordFile bool) map[string]any {
	if result == nil {
		return map[string]any{}
	}
	downloadURL := legacyContentVersionDownloadURL(version, result.ID)
	if recordFile {
		downloadURL = recordFileDownloadURL(version, result.ID)
	}
	return map[string]any{
		"Id":          result.ID,
		"Name":        result.Name,
		"Size":        result.Size,
		"Mime":        result.Mime,
		"DownloadUrl": downloadURL,
	}
}

func fileDownloadResponse(download *contract.FileDownload) *dialect.Response {
	if download == nil || download.Reader == nil {
		return notFoundResponse("File not found")
	}
	contentLength := download.Size
	contentType := safeFileDownloadContentType(download.ContentType)
	return &dialect.Response{
		Status: http.StatusOK,
		Headers: map[string]string{
			"Content-Disposition": fmt.Sprintf("attachment; filename=\"%s\"", safeDownloadFileName(download.FileName)),
		},
		Stream: &dialect.ResponseStream{
			Reader:        download.Reader,
			ContentType:   contentType,
			ContentLength: &contentLength,
		},
	}
}

func validateLegacyContentVersionID(fileID string) *dialect.Response {
	if isLegacyContentVersionID(fileID) {
		return nil
	}
	return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "Invalid file ID")}
}

func isLegacyContentVersionID(fileID string) bool {
	if strings.Contains(fileID, "/") || strings.Contains(fileID, "\\") || strings.Contains(fileID, "..") {
		return false
	}
	ext := strings.ToLower(filepath.Ext(fileID))
	if !allowedFileExtensions[ext] {
		return false
	}
	stem := strings.TrimSuffix(fileID, ext)
	if !legacyContentVersionUUIDStem.MatchString(stem) {
		return false
	}
	return true
}

func fileOperationErrorResponse(req *dialect.Request, err error, defaultMessage string) *dialect.Response {
	if err == nil {
		return nil
	}
	if sharederrors.IsInternal(err) {
		return &dialect.Response{Status: http.StatusInternalServerError, Body: wire.SFErrorResponse(wire.SFCodeInternalError, defaultMessage)}
	}
	if resp := respondServiceError(req, err); resp != nil {
		return resp
	}
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(req.Version)); ok {
		return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
	}
	return &dialect.Response{Status: http.StatusInternalServerError, Body: wire.SFErrorResponse(wire.SFCodeInternalError, defaultMessage)}
}

func fileDownloadErrorResponse(req *dialect.Request, err error) *dialect.Response {
	if err == nil {
		return nil
	}
	if sfErr, status, ok := wire.SFErrorFromDomainForVersion(err, apiVersionOrDefault(req.Version)); ok {
		return &dialect.Response{Status: status, Body: []wire.SFError{sfErr}}
	}
	return fileOperationErrorResponse(req, err, "Failed to read file")
}

func noFileProvidedResponse() *dialect.Response {
	return &dialect.Response{Status: http.StatusBadRequest, Body: wire.SFErrorResponse(wire.SFCodeInvalidRequest, "No file provided")}
}

func fileTooLargeResponse() *dialect.Response {
	return &dialect.Response{
		Status: http.StatusRequestEntityTooLarge,
		Body:   wire.SFErrorResponse(wire.SFCodeInvalidRequest, fmt.Sprintf("File exceeds %dMB limit", maxV63FileUploadSizeBytes/(1024*1024))),
	}
}

func normalizeFileMediaType(contentType string) string {
	mediaType, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		return strings.ToLower(strings.TrimSpace(contentType))
	}
	return strings.ToLower(strings.TrimSpace(mediaType))
}

func safeFileDownloadContentType(contentType string) string {
	contentType = strings.TrimSpace(contentType)
	if contentType == "" || blockedFileMIMEs[normalizeFileMediaType(contentType)] {
		return "application/octet-stream"
	}
	return contentType
}

func firstFileHeader(headers map[string][]string, name string) string {
	for key, values := range headers {
		if strings.EqualFold(key, name) && len(values) > 0 {
			return values[0]
		}
	}
	return ""
}

func safeDownloadFileName(fileName string) string {
	name := strings.ReplaceAll(filepath.Base(fileName), "\"", "_")
	if strings.TrimSpace(name) == "" || name == "." || name == "/" {
		return "file"
	}
	return name
}

func recordFileDownloadURL(version, fileID string) string {
	return fmt.Sprintf("/services/data/%s/files/%s/download", apiVersionOrDefault(version), fileID)
}

func legacyContentVersionDownloadURL(version, fileID string) string {
	return fmt.Sprintf("/services/data/%s/sobjects/ContentVersion/%s/VersionData", apiVersionOrDefault(version), fileID)
}

func newLegacyContentVersionID(ext string) (string, error) {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", err
	}
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	return fmt.Sprintf("%x-%x-%x-%x-%x%s", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16], ext), nil
}

func legacyContentVersionStorageKey(fileID string) string {
	return path.Join("contentversion", fileID)
}

func legacyContentVersionBlobReference(fileID string) contract.FileBlobReference {
	return contract.FileBlobReference{
		StorageKey:     legacyContentVersionStorageKey(fileID),
		LegacyFileName: fileID,
		FileName:       fileID,
	}
}

func ResolveContentVersionBlobReference(reference string) (contract.FileBlobReference, bool) {
	fileID, ok := legacyContentVersionIDFromReference(reference)
	if !ok {
		return contract.FileBlobReference{}, false
	}
	return legacyContentVersionBlobReference(fileID), true
}

func DetachedFileReferenceResolver() func(string) (contract.FileBlobReference, bool) {
	return ResolveContentVersionBlobReference
}

func legacyContentVersionIDFromReference(reference string) (string, bool) {
	reference = strings.TrimSpace(reference)
	if reference == "" {
		return "", false
	}
	const routePrefix = "/sobjects/ContentVersion/"
	const routeSuffix = "/VersionData"
	if idx := strings.Index(reference, routePrefix); idx >= 0 {
		candidate := reference[idx+len(routePrefix):]
		if cut := strings.Index(candidate, routeSuffix); cut >= 0 {
			reference = strings.TrimSpace(candidate[:cut])
		}
	}
	if !isLegacyContentVersionID(reference) {
		return "", false
	}
	return reference, true
}
