package com.gzzm.lobster.storage;

import com.gzzm.lobster.common.IdGenerator;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.config.LobsterConfig;
import net.cyan.arachne.annotation.Service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

/**
 * FileSystemContentStore —— 本地/NFS 磁盘实现 / Local or NFS disk implementation.
 *
 * <p>政务外网部署常见形态：本地盘或内网 NFS 共享存储；
 * 对象存储（MinIO 等）可按相同接口实现替换。
 */
@Service
public class FileSystemContentStore implements ContentStore {

    private static final DateTimeFormatter YEAR = DateTimeFormatter.ofPattern("yyyy");
    private static final DateTimeFormatter MONTH = DateTimeFormatter.ofPattern("MM");
    private static final DateTimeFormatter DAY = DateTimeFormatter.ofPattern("dd");

    private final Path root;

    /**
     * 零参构造器——供 nest 自动实例化使用。
     * 根目录选取：{@code $CATALINA_BASE/files/lobster-content}；若无 catalina.base
     * 环境（非 Tomcat），退回 {@code $CWD/files/lobster-content}。
     */
    public FileSystemContentStore() {
        this(defaultRoot());
    }

    public FileSystemContentStore(String rootPath) {
        this.root = Paths.get(rootPath).toAbsolutePath().normalize();
        try {
            Files.createDirectories(this.root);
        } catch (IOException e) {
            throw new LobsterException("storage.init", "Cannot create content store root: " + rootPath, e);
        }
    }

    private static String defaultRoot() {
        String base = LobsterConfig.getContentStoreBase();
        if (base == null || base.isEmpty()) {
            base = System.getProperty("catalina.base");
            if (base == null || base.isEmpty()) {
                base = System.getProperty("user.dir", ".");
            }
        }
        return base + "/files/lobster-content";
    }

    /** 计算写入路径 / Compute the storage path. */
    private String buildRef(String category, String userId, String format) {
        LocalDate now = LocalDate.now();
        String ext = (format == null || format.isEmpty()) ? "bin" : format;
        String uid = (userId == null || userId.isEmpty()) ? "_sys" : userId;
        return category + "/" + now.format(YEAR) + "/" + now.format(MONTH)
                + "/" + now.format(DAY) + "/" + uid + "/"
                + IdGenerator.uuid() + "." + ext;
    }

    private Path resolveSafe(String contentRef) {
        Path p = root.resolve(contentRef).normalize();
        if (!p.startsWith(root)) {
            // 路径穿越防护 / Path traversal guard
            throw new LobsterException("storage.path", "Invalid contentRef: " + contentRef);
        }
        return p;
    }

    @Override
    public String write(String category, String userId, String content, String format) {
        return writeBinary(category, userId, content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8), format);
    }

    @Override
    public String writeBinary(String category, String userId, byte[] bytes, String format) {
        String ref = buildRef(category, userId, format);
        Path p = resolveSafe(ref);
        try {
            Files.createDirectories(p.getParent());
            Files.write(p, bytes == null ? new byte[0] : bytes,
                    StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
            return ref;
        } catch (IOException e) {
            throw new LobsterException("storage.write", "Write failed: " + ref, e);
        }
    }

    @Override
    public String read(String contentRef) {
        try {
            Path p = resolveSafe(contentRef);
            if (!Files.exists(p)) return null;
            return new String(Files.readAllBytes(p), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new LobsterException("storage.read", "Read failed: " + contentRef, e);
        }
    }

    @Override
    public String read(String contentRef, int offset, int limit) {
        String full = read(contentRef);
        if (full == null) return null;
        if (offset >= full.length()) return "";
        int end = Math.min(full.length(), offset + limit);
        return full.substring(offset, end);
    }

    @Override
    public byte[] readBinary(String contentRef) {
        try {
            Path p = resolveSafe(contentRef);
            if (!Files.exists(p)) return null;
            return Files.readAllBytes(p);
        } catch (IOException e) {
            throw new LobsterException("storage.read", "Read binary failed: " + contentRef, e);
        }
    }

    @Override
    public byte[] readBinary(String contentRef, int offset, int limit) {
        try {
            Path p = resolveSafe(contentRef);
            if (!Files.exists(p)) return null;
            long total = Files.size(p);
            if (offset < 0) offset = 0;
            if (offset >= total) return new byte[0];
            long remain = total - offset;
            int readLen = (limit <= 0) ? (int) Math.min(remain, Integer.MAX_VALUE)
                                       : (int) Math.min(remain, (long) limit);
            byte[] buf = new byte[readLen];
            try (java.io.InputStream in = Files.newInputStream(p)) {
                long skipped = 0;
                while (skipped < offset) {
                    long s = in.skip(offset - skipped);
                    if (s <= 0) break;
                    skipped += s;
                }
                int off = 0;
                while (off < readLen) {
                    int r = in.read(buf, off, readLen - off);
                    if (r < 0) break;
                    off += r;
                }
                if (off < readLen) {
                    byte[] trimmed = new byte[off];
                    System.arraycopy(buf, 0, trimmed, 0, off);
                    return trimmed;
                }
                return buf;
            }
        } catch (IOException e) {
            throw new LobsterException("storage.read", "Read binary range failed: " + contentRef, e);
        }
    }

    @Override
    public void delete(String contentRef) {
        try {
            Path p = resolveSafe(contentRef);
            Files.deleteIfExists(p);
        } catch (IOException e) {
            throw new LobsterException("storage.delete", "Delete failed: " + contentRef, e);
        }
    }

    @Override
    public long getSize(String contentRef) {
        try {
            Path p = resolveSafe(contentRef);
            return Files.exists(p) ? Files.size(p) : 0L;
        } catch (IOException e) {
            throw new LobsterException("storage.size", "Size failed: " + contentRef, e);
        }
    }

    @Override
    public boolean exists(String contentRef) {
        try {
            return Files.exists(resolveSafe(contentRef));
        } catch (Exception e) {
            return false;
        }
    }
}
