package main

import (
	"archive/zip"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
)

const (
	defArchiveFilename = "templates.boardarchive"
	versionFilename    = "version.json"
	boardFilename      = "board.jsonl"
	minArchiveVersion  = 2
	maxArchiveVersion  = 2
)

type archiveVersion struct {
	Version int   `json:"version"`
	Date    int64 `json:"date"`
}

type appConfig struct {
	dir     string
	out     string
	verbose bool
}

func main() {
	cfg := appConfig{}

	flag.StringVar(&cfg.dir, "dir", "", "source directory of templates")
	flag.StringVar(&cfg.out, "out", defArchiveFilename, "output filename")
	flag.BoolVar(&cfg.verbose, "verbose", false, "enable verbose output")
	flag.Parse()

	if cfg.dir == "" {
		flag.Usage()
		os.Exit(-1)
	}

	var code int
	if err := build(cfg); err != nil {
		code = -1
		fmt.Fprintf(os.Stderr, "error creating archive: %v\n", err)
	} else if cfg.verbose {
		fmt.Fprintf(os.Stdout, "archive created: %s\n", cfg.out)
	}

	os.Exit(code)
}

func build(cfg appConfig) (err error) {
	version, err := getVersionFile(cfg)
	if err != nil {
		return err
	}

	// create the output archive zip file
	archiveFile, err := os.Create(cfg.out)
	if err != nil {
		return fmt.Errorf("error creating %s: %w", cfg.out, err)
	}
	archiveZip := zip.NewWriter(archiveFile)
	defer func() {
		if err2 := archiveZip.Close(); err2 != nil {
			if err == nil {
				err = fmt.Errorf("error closing zip %s: %w", cfg.out, err2)
			}
		}
		if err2 := archiveFile.Close(); err2 != nil {
			if err == nil {
				err = fmt.Errorf("error closing %s: %w", cfg.out, err2)
			}
		}
	}()

	// write the version file
	v, err := archiveZip.Create(versionFilename)
	if err != nil {
		return fmt.Errorf("error creating %s: %w", cfg.out, err)
	}
	if _, err = v.Write(version); err != nil {
		return fmt.Errorf("error writing %s: %w", cfg.out, err)
	}

	// each board is a subdirectory; write each to the archive
	files, err := ioutil.ReadDir(cfg.dir)
	if err != nil {
		return fmt.Errorf("error reading directory %s: %w", cfg.dir, err)
	}
	for _, f := range files {
		if !f.IsDir() {
			if f.Name() != versionFilename && cfg.verbose {
				fmt.Fprintf(os.Stdout, "skipping non-directory %s\n", f.Name())
			}
			continue
		}
		if err = writeBoard(archiveZip, f.Name(), cfg); err != nil {
			return fmt.Errorf("error writing board %s: %w", f.Name(), err)
		}
	}
	return nil
}

func getVersionFile(cfg appConfig) ([]byte, error) {
	path := filepath.Join(cfg.dir, versionFilename)
	buf, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("cannot read %s: %w", path, err)
	}

	var version archiveVersion
	if err := json.Unmarshal(buf, &version); err != nil {
		return nil, fmt.Errorf("cannot parse %s: %w", path, err)
	}

	if version.Version < minArchiveVersion || version.Version > maxArchiveVersion {
		return nil, errUnsupportedVersion{Min: minArchiveVersion, Max: maxArchiveVersion, Got: version.Version}
	}

	return buf, nil
}

func writeBoard(w *zip.Writer, boardID string, cfg appConfig) error {
	// copy the board's jsonl file first.  BoardID is also the directory name.
	srcPath := filepath.Join(cfg.dir, boardID, boardFilename)
	destPath := filepath.Join(boardID, boardFilename)
	if err := writeFile(w, srcPath, destPath, cfg); err != nil {
		return err
	}

	boardPath := filepath.Join(cfg.dir, boardID)
	files, err := ioutil.ReadDir(boardPath)
	if err != nil {
		return fmt.Errorf("error reading board directory %s: %w", cfg.dir, err)
	}
	for _, f := range files {
		if f.IsDir() {
			if cfg.verbose {
				fmt.Fprintf(os.Stdout, "skipping directory %s\n", f.Name())
			}
			continue
		}
		if f.Name() == boardFilename {
			continue
		}

		srcPath = filepath.Join(cfg.dir, boardID, f.Name())
		destPath = filepath.Join(boardID, f.Name())
		if err = writeFile(w, srcPath, destPath, cfg); err != nil {
			return fmt.Errorf("error writing %s: %w", destPath, err)
		}
	}
	return nil
}

func writeFile(w *zip.Writer, srcPath string, destPath string, cfg appConfig) (err error) {
	inFile, err := os.Open(srcPath)
	if err != nil {
		return fmt.Errorf("error reading %s: %w", srcPath, err)
	}
	defer inFile.Close()

	outFile, err := w.Create(destPath)
	if err != nil {
		return fmt.Errorf("error creating %s: %w", destPath, err)
	}
	size, err := io.Copy(outFile, inFile)
	if err != nil {
		return fmt.Errorf("error writing %s: %w", destPath, err)
	}

	if cfg.verbose {
		fmt.Fprintf(os.Stdout, "%s written (%d bytes)\n", destPath, size)
	}

	return nil
}

type errUnsupportedVersion struct {
	Min int
	Max int
	Got int
}

func (e errUnsupportedVersion) Error() string {
	return fmt.Sprintf("unsupported archive version; require between %d and %d inclusive, got %d", e.Min, e.Max, e.Got)
}
