package leigh.tansu.service;

import leigh.eo.EO;
import leigh.mecha.io.OutputFile;
import leigh.mecha.io.Persister;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.util.IOUtil;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * A simple implementation of a storage store using the local filesystem. This is great for dev/test environments
 * and very simple single-server production deployments. Files are stored on disk as zip files which contain both
 * the meta parts and data parts of the object. This allows the files to be read/written/moved/deleted atomically,
 * while providing compression, but maintains an industry standard approach.
 * <p>
 * This implementation takes a bath path and stores everything within that path. In the basepath directory
 * are n subdirectories, name after the UUID of the bucket they represent. Within each subdirectory is a folder
 * containing zip files, each of which represents a single object.
 *
 * @author Alex Leigh
 */
public class TansuFileStore {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(TansuFileStore.class);
    protected static final String MAIN_FILENAME = "main.dat";
    protected static final String META_FILENAME = "meta.eo";
    public static final String META_CONTENT_TYPE_KEY = "contentType";

    private final Persister persister = new Persister(Path.of("tansu"));

    public TansuFileStore() throws IOException, ClassNotFoundException {
        String objId = ".. e /// evil hello";
        String bucketId = "test";

        createBucket(bucketId);

        write(bucketId, objId,
                new ByteArrayInputStream("Hello World!\n".getBytes(StandardCharsets.UTF_8)), "text/plain"
        );

        try (TansuFileObject obj = read(bucketId, objId)) {
            EO meta = obj.readMeta();
            logger.info("OBJ: [meta: {}] [obj: {}]", meta, obj);
        }
    }

    private String makeSafe(String s) {
        return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8));
    }

    public void createBucket(String bucketId) {
        logger.info("Creating bucket. [bucketId: {}]", bucketId);
        persister.getBasePath().resolve(makeSafe(bucketId)).toFile().mkdirs();
    }

    private Path objectPath(String bucketId, String objId) {
        return Path.of(makeSafe(bucketId),
                makeSafe(objId));
    }

    public void write(String bucketId, String objId, InputStream data, String contentType) throws IOException {
        OutputFile outFile = persister.output(objectPath(bucketId, objId));
        logger.info("write() called. [outFile: {}]", outFile);
        ZipOutputStream zos = new ZipOutputStream(outFile.getOutputStream());
        // The order matters here: must match TansuFileObject
        zos.putNextEntry(new ZipEntry(MAIN_FILENAME));
        IOUtil.copy(data, zos);
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry(META_FILENAME));
        ObjectOutputStream oos = new ObjectOutputStream(zos);
        oos.writeObject(new EO().setValue(META_CONTENT_TYPE_KEY, contentType));
        oos.close();
        // We don't close the entry, oos did that. It is poorly behaved and will throw if we close it twice.
        zos.close();
        outFile.commit();
    }

    public TansuFileObject read(String bucketId, String objId) throws IOException {
        Path fullpath = persister.getBasePath().resolve(objectPath(bucketId, objId));
        logger.info("read(). [path: {}] [len: {}]", fullpath, fullpath.toFile().length());
        return new TansuFileObject(fullpath);
    }
}
