package controller

import (
	"encoding/base64"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-errors/errors"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/oapi-codegen/nullable"
	"github.com/phishingclub/phishingclub/data"
	"github.com/phishingclub/phishingclub/database"
	"github.com/phishingclub/phishingclub/errs"
	"github.com/phishingclub/phishingclub/model"
	"github.com/phishingclub/phishingclub/repository"
	"github.com/phishingclub/phishingclub/service"
	"github.com/phishingclub/phishingclub/utils"
	"github.com/phishingclub/phishingclub/vo"
)

// AttachmentColumnsMap is a map between the frontend and the backend
// so the frontend has user friendly names instead of direct references
// to the database schema
// this is tied to a slice in the repository package
var AttachmentColumnsMap = map[string]string{
	"created_at":       repository.TableColumn(database.ATTACHMENT_TABLE, "created_at"),
	"updated_at":       repository.TableColumn(database.ATTACHMENT_TABLE, "updated_at"),
	"name":             repository.TableColumn(database.ATTACHMENT_TABLE, "name"),
	"description":      repository.TableColumn(database.ATTACHMENT_TABLE, "description"),
	"embedded content": repository.TableColumn(database.ATTACHMENT_TABLE, "embeddedContent"),
	"send as calendar": repository.TableColumn(database.ATTACHMENT_TABLE, "sendAsCalendar"),
	"filename":         repository.TableColumn(database.ATTACHMENT_TABLE, "filename"),
}

// Attachment is an static Attachment controller
type Attachment struct {
	Common
	StaticAttachmentPath string
	TemplateService      *service.Template
	AttachmentService    *service.Attachment
	OptionService        *service.Option
	CompanyService       *service.Company
}

// GetContentByID returns the content and mime type of an attachment
func (a *Attachment) GetContentByID(g *gin.Context) {
	session, _, ok := a.handleSession(g)
	if !ok {
		return
	}
	// parse request
	id, ok := a.handleParseIDParam(g)
	if !ok {
		return
	}
	// get the attachment
	ctx := g.Request.Context()
	attachment, err := a.AttachmentService.GetByID(
		ctx,
		session,
		id,
	)
	if ok := a.handleErrors(g, err); !ok {
		return
	}
	p := attachment.Path.MustGet().String()
	// serve the file
	// #nosec
	content, err := os.ReadFile(p)
	if err != nil {
		a.Logger.Errorw("failed to read file",
			"path", p,
			"error", err,
		)
		a.Response.ServerError(g)
		return
	}

	fileExt := filepath.Ext(p)
	mimeType := ""
	switch fileExt {
	case ".html":
		mimeType = "text/html"
	case ".htm":
		mimeType = "text/html"
	case ".xhtml":
		mimeType = "application/xhtml+xml"
	default:
		mimeType = http.DetectContentType(content)
	}
	// get by id is only used for admin viewing of an attachemnt, so all
	// embedded content must contain example data
	if attachment.EmbeddedContent.MustGet() {
		// build email
		domain := &model.Domain{
			Name: nullable.NewNullableWithValue(
				*vo.NewString255Must("example.test"),
			),
		}
		recipient := model.NewRecipientExample()
		campaignRecipient := model.CampaignRecipient{
			ID: nullable.NewNullableWithValue(
				uuid.New(),
			),
			Recipient: recipient,
		}
		email := model.NewEmailExample()
		// hacky
		email.Content = nullable.NewNullableWithValue(
			*vo.NewUnsafeOptionalString1MB(string(content)),
		)
		apiSender := model.NewAPISenderExample()
		b, err := a.TemplateService.CreateMailBody(
			g.Request.Context(),
			"id",
			"/foo",
			domain,
			&campaignRecipient,
			email,
			apiSender,
			nil, // no company context for attachment preview
		)
		if err != nil {
			a.Logger.Errorw("failed to appy template to attachment",
				"error", err,
			)
			a.Response.ServerError(g)
			return
		}
		content = []byte(b)
	}
	a.Response.OK(g, gin.H{
		"mimeType": mimeType,
		"file":     base64.StdEncoding.EncodeToString(content),
	})
}

// GetAllForContext gets all attachments for a domain
// and has a special case 'shared' to get all global attachments
func (a *Attachment) GetAllForContext(g *gin.Context) {
	session, _, ok := a.handleSession(g)
	if !ok {
		return
	}
	// check permissions
	isAuthorized, err := service.IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL)
	if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) {
		a.Logger.Errorw("failed to check permissions",
			"error", err,
		)
		a.Response.ServerError(g)
		return
	}
	if !isAuthorized {
		// TODO audit log
		_ = handleAuthorizationError(g, a.Response, errs.ErrAuthorizationFailed)
		return
	}
	// parse request
	companyID := companyIDFromRequestQuery(g)
	// if there is no companyID then it is a global attachment request
	// else the company context name is the attachment scope
	if companyID != nil {
		// get the company id and to check if the user has permission to retrieve it
		_, err := a.CompanyService.GetByID(
			g.Request.Context(),
			session,
			companyID,
		)
		if ok := a.handleErrors(g, err); !ok {
			return
		}
	}
	queryArgs, ok := a.handleQueryArgs(g)
	if !ok {
		return
	}
	queryArgs.DefaultSortByUpdatedAt()
	queryArgs.RemapOrderBy(AttachmentColumnsMap)
	// get attachments
	a.Logger.Debugw("getting attachments for company ID",
		"companyID", companyID,
	)
	attachments, err := a.AttachmentService.GetAll(
		g,
		session,
		companyID,
		queryArgs,
	)
	// handle responses
	if ok := a.handleErrors(g, err); !ok {
		return
	}
	a.Response.OK(g, attachments)
}

// Create uploads an attachment
func (a *Attachment) Create(g *gin.Context) {
	session, _, ok := a.handleSession(g)
	if !ok {
		return
	}
	// parse request
	multipartData, err := g.MultipartForm()
	if err != nil {
		a.Logger.Errorw("failed to get multipart form",
			"error", err,
		)
		a.Response.BadRequest(g)
		return
	}
	if len(multipartData.File["files"]) == 0 {
		a.Logger.Debug("no files to upload")
		a.Response.BadRequestMessage(g, "No files selected")
		return
	}
	companyID := nullable.NewNullNullable[uuid.UUID]()
	companyIDParam := g.PostForm("companyID")
	if len(companyIDParam) > 0 {
		cid, err := uuid.Parse(companyIDParam)
		if err != nil {
			a.Logger.Debugw("failed to parse company id",
				"error", err,
			)
			a.Response.ValidationFailed(g, "companyID", err)
			return
		}
		companyID.Set(cid)
	}
	nameParam, err := vo.NewOptionalString127(g.PostForm("name"))
	if err != nil {
		a.Logger.Debugw("failed to parse name",
			"name", g.PostForm("name"),
			"error", err,
		)
		a.Response.ValidationFailed(g, "name", err)
		return
	}
	name := nullable.NewNullableWithValue(*nameParam)
	descriptionParam, err := vo.NewOptionalString255(g.PostForm("description"))
	if err != nil {
		a.Logger.Debugw("failed to parse description",
			"error", err,
		)
		a.Response.ValidationFailed(g, "description", err)
		return
	}
	description := nullable.NewNullableWithValue(*descriptionParam)
	embeddedContent := nullable.NewNullableWithValue(false)
	embeddedContentString := g.PostForm("embeddedContent")
	if strings.ToLower(embeddedContentString) == "true" {
		embeddedContent.Set(true)
	}
	sendAsCalendar := nullable.NewNullableWithValue(false)
	sendAsCalendarString := g.PostForm("sendAsCalendar")
	if strings.ToLower(sendAsCalendarString) == "true" {
		sendAsCalendar.Set(true)
	}
	attachments := []*model.Attachment{}
	for _, file := range multipartData.File["files"] {
		// TODO multi user validate that the company id is the same as the session company id or that the session is a super admin
		// check max file size
		maxFile, err := a.OptionService.GetOption(g, session, data.OptionKeyMaxFileUploadSizeMB)
		if ok := a.handleErrors(g, err); !ok {
			return
		}
		ok, err := utils.CompareFileSizeFromString(file.Size, maxFile.Value.String())
		if err != nil {
			a.Logger.Errorw("failed to compare file size",
				"error", err,
			)
		}
		if !ok {
			a.Logger.Debugw("file too large",
				"filename", file.Filename,
				"size", file.Size,
				"maxSize", maxFile.Value.String(),
			)
			a.Response.ValidationFailed(
				g,
				"File",
				fmt.Errorf("'%s' is too large", utils.ReadableFileName(file.Filename)),
			)
			return
		}
		fileNameParam, err := vo.NewFileName(file.Filename)
		if err != nil {
			a.Logger.Debugw("failed to parse filename",
				"error", err,
			)
			a.Response.ValidationFailed(g, "filename", err)
			return
		}
		fileName := nullable.NewNullableWithValue(*fileNameParam)

		attachment := model.Attachment{
			CompanyID:       companyID,
			Name:            name,
			Description:     description,
			EmbeddedContent: embeddedContent,
			SendAsCalendar:  sendAsCalendar,
			File:            file,
			FileName:        fileName,
		}
		if err := attachment.Validate(); err != nil {
			a.Logger.Debugw("failed to validate attachment",
				"attachmentName", name,
				"error", err,
			)
			a.Response.ValidationFailed(g, "attachment", err)
			return
		}
		attachments = append(attachments, &attachment)
	}
	//  store the files on disk and in database
	createdIDs, err := a.AttachmentService.Create(
		g,
		session,
		attachments,
	)
	if ok := a.handleErrors(g, err); !ok {
		return
	}
	a.Response.OK(g, gin.H{
		"ids":            createdIDs,
		"files_uploaded": len(attachments),
	})
}

// GetByID gets an static attachment by id
func (a *Attachment) GetByID(g *gin.Context) {
	session, _, ok := a.handleSession(g)
	if !ok {
		return
	}
	// parse request
	id, ok := a.handleParseIDParam(g)
	if !ok {
		return
	}
	// get the attachment
	ctx := g.Request.Context()
	attachment, err := a.AttachmentService.GetByID(
		ctx,
		session,
		id,
	)
	if ok := a.handleErrors(g, err); !ok {
		return
	}
	a.Response.OK(g, attachment)
}

// UpdateByID updates an static attachment by id
func (a *Attachment) UpdateByID(g *gin.Context) {
	// handle session
	session, _, ok := a.handleSession(g)
	if !ok {
		return
	}
	id, ok := a.handleParseIDParam(g)
	if !ok {
		return
	}
	// parse request
	var req model.Attachment
	if ok := a.handleParseRequest(g, &req); !ok {
		return
	}
	// update the attachment
	ctx := g.Request.Context()
	err := a.AttachmentService.UpdateByID(
		ctx,
		session,
		id,
		&req,
	)
	if ok := a.handleErrors(g, err); !ok {
		return
	}
	a.Response.OK(g, gin.H{})
}

// RemoveByID removes an static attachment
// if the attachment is a directory, it will be removed recursively
func (a *Attachment) RemoveByID(g *gin.Context) {
	// handle session
	session, _, ok := a.handleSession(g)
	if !ok {
		return
	}
	// parse request
	id, ok := a.handleParseIDParam(g)
	if !ok {
		return
	}
	// remove the attachment
	ctx := g.Request.Context()
	err := a.AttachmentService.DeleteByID(
		ctx,
		session,
		id,
	)
	if ok := a.handleErrors(g, err); !ok {
		return
	}
	a.Response.OK(g, gin.H{})
}
