package app

import (
	"archive/zip"
	"encoding/json"
	"fmt"
	"io"

	"github.com/mattermost/focalboard/server/model"
	"github.com/wiggin77/merror"

	"github.com/mattermost/mattermost-server/v6/shared/mlog"
)

var (
	newline = []byte{'\n'}
)

func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) {
	boards, err := a.getBoardsForArchive(opt.BoardIDs)
	if err != nil {
		return err
	}

	merr := merror.New()
	defer func() {
		errs = merr.ErrorOrNil()
	}()

	// wrap the writer in a zip.
	zw := zip.NewWriter(w)
	defer func() {
		merr.Append(zw.Close())
	}()

	if err := a.writeArchiveVersion(zw); err != nil {
		merr.Append(err)
		return
	}

	for _, board := range boards {
		if err := a.writeArchiveBoard(zw, board, opt); err != nil {
			merr.Append(fmt.Errorf("cannot export board %s: %w", board.ID, err))
			return
		}
	}
	return nil
}

// writeArchiveVersion writes a version file to the zip.
func (a *App) writeArchiveVersion(zw *zip.Writer) error {
	archiveHeader := model.ArchiveHeader{
		Version: archiveVersion,
		Date:    model.GetMillis(),
	}
	b, _ := json.Marshal(&archiveHeader)

	w, err := zw.Create("version.json")
	if err != nil {
		return fmt.Errorf("cannot write archive header: %w", err)
	}

	if _, err := w.Write(b); err != nil {
		return fmt.Errorf("cannot write archive header: %w", err)
	}
	return nil
}

// writeArchiveBoard writes a single board to the archive in a zip directory.
func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error {
	// create a directory per board
	w, err := zw.Create(board.ID + "/board.jsonl")
	if err != nil {
		return err
	}

	// write the board block first
	if err = a.writeArchiveBoardLine(w, board); err != nil {
		return err
	}

	var files []string
	// write the board's blocks
	// TODO: paginate this
	blocks, err := a.GetBlocksWithBoardID(board.ID)
	if err != nil {
		return err
	}

	for _, block := range blocks {
		if err = a.writeArchiveBlockLine(w, block); err != nil {
			return err
		}
		if block.Type == model.TypeImage {
			filename, err := extractImageFilename(block)
			if err != nil {
				return err
			}
			files = append(files, filename)
		}
	}

	// write the files
	for _, filename := range files {
		if err := a.writeArchiveFile(zw, filename, board.ID, opt); err != nil {
			return fmt.Errorf("cannot write file %s to archive: %w", filename, err)
		}
	}
	return nil
}

// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBlockLine(w io.Writer, block model.Block) error {
	b, err := json.Marshal(&block)
	if err != nil {
		return err
	}
	line := model.ArchiveLine{
		Type: "block",
		Data: b,
	}

	b, err = json.Marshal(&line)
	if err != nil {
		return err
	}

	_, err = w.Write(b)
	if err != nil {
		return err
	}

	// jsonl files need a newline
	_, err = w.Write(newline)
	return err
}

// writeArchiveBlockLine writes a single block to the archive.
func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error {
	b, err := json.Marshal(&board)
	if err != nil {
		return err
	}
	line := model.ArchiveLine{
		Type: "board",
		Data: b,
	}

	b, err = json.Marshal(&line)
	if err != nil {
		return err
	}

	_, err = w.Write(b)
	if err != nil {
		return err
	}

	// jsonl files need a newline
	_, err = w.Write(newline)
	return err
}

// writeArchiveFile writes a single file to the archive.
func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error {
	dest, err := zw.Create(boardID + "/" + filename)
	if err != nil {
		return err
	}

	src, err := a.GetFileReader(opt.TeamID, boardID, filename)
	if err != nil {
		// just log this; image file is missing but we'll still export an equivalent board
		a.logger.Error("image file missing for export",
			mlog.String("filename", filename),
			mlog.String("team_id", opt.TeamID),
			mlog.String("board_id", boardID),
		)
		return nil
	}
	defer src.Close()

	_, err = io.Copy(dest, src)
	return err
}

// getBoardsForArchive fetches all the specified boards.
func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) {
	boards := make([]model.Board, 0, len(boardIDs))

	for _, id := range boardIDs {
		b, err := a.GetBoard(id)
		if err != nil {
			return nil, fmt.Errorf("could not fetch board %s: %w", id, err)
		}

		boards = append(boards, *b)
	}
	return boards, nil
}

func extractImageFilename(imageBlock model.Block) (string, error) {
	f, ok := imageBlock.Fields["fileId"]
	if !ok {
		return "", model.ErrInvalidImageBlock
	}

	filename, ok := f.(string)
	if !ok {
		return "", model.ErrInvalidImageBlock
	}
	return filename, nil
}
