package io.cameleer.server.core.runtime;

import io.cameleer.server.core.storage.ArtifactCoordinates;
import io.cameleer.server.core.storage.ArtifactStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Pattern;

public class AppService {
    private static final Logger log = LoggerFactory.getLogger(AppService.class);

    /** Slug rules mirror {@link EnvironmentService#SLUG_PATTERN}: lowercase letters, digits, hyphens; 1–64 chars; starts with alnum. Immutable after creation. */
    private static final Pattern SLUG_PATTERN = Pattern.compile("^[a-z0-9][a-z0-9-]{0,63}$");

    private final AppRepository appRepo;
    private final AppVersionRepository versionRepo;
    private final ArtifactStore artifactStore;
    private final String tenantId;
    private final CreateGuard createGuard;

    public AppService(AppRepository appRepo, AppVersionRepository versionRepo,
                      ArtifactStore artifactStore, String tenantId) {
        this(appRepo, versionRepo, artifactStore, tenantId, CreateGuard.NOOP);
    }

    public AppService(AppRepository appRepo, AppVersionRepository versionRepo,
                      ArtifactStore artifactStore, String tenantId, CreateGuard createGuard) {
        this.appRepo = appRepo;
        this.versionRepo = versionRepo;
        this.artifactStore = artifactStore;
        this.tenantId = tenantId;
        this.createGuard = createGuard;
    }

    public List<App> listAll() { return appRepo.findAll(); }
    public List<App> listByEnvironment(UUID environmentId) { return appRepo.findByEnvironmentId(environmentId); }
    public App getById(UUID id) { return appRepo.findById(id).orElseThrow(() -> new IllegalArgumentException("App not found: " + id)); }
    public App getBySlug(String slug) { return appRepo.findBySlug(slug).orElseThrow(() -> new IllegalArgumentException("App not found: " + slug)); }
    public App getByEnvironmentAndSlug(UUID environmentId, String slug) {
        return appRepo.findByEnvironmentIdAndSlug(environmentId, slug)
                .orElseThrow(() -> new IllegalArgumentException("App not found in environment: " + slug));
    }
    public List<AppVersion> listVersions(UUID appId) { return versionRepo.findByAppId(appId); }

    public AppVersion getVersion(UUID versionId) {
        return versionRepo.findById(versionId)
                .orElseThrow(() -> new IllegalArgumentException("AppVersion not found: " + versionId));
    }

    /** Typed Optional accessor for callers that distinguish missing-rows from
     * other failures without relying on the message-based exception convention
     * of {@link #getVersion(UUID)}. */
    public Optional<AppVersion> findVersion(UUID versionId) {
        return versionRepo.findById(versionId);
    }

    /** Coordinate corresponding to an AppVersion. Used by callers that need to
     *  pass the artifact identity to {@link ArtifactStore} (download, delete, exists). */
    public ArtifactCoordinates coordinatesFor(AppVersion version) {
        return new ArtifactCoordinates(tenantId, version.appId(), version.version());
    }

    public void updateContainerConfig(UUID id, Map<String, Object> containerConfig) {
        getById(id); // verify exists
        appRepo.updateContainerConfig(id, containerConfig);
    }

    public UUID createApp(UUID environmentId, String slug, String displayName) {
        if (slug == null || !SLUG_PATTERN.matcher(slug).matches()) {
            throw new IllegalArgumentException(
                    "Invalid app slug: must match ^[a-z0-9][a-z0-9-]{0,63}$ (lowercase letters, digits, hyphens)");
        }
        createGuard.check(appRepo.count());
        if (appRepo.findByEnvironmentIdAndSlug(environmentId, slug).isPresent()) {
            throw new IllegalArgumentException("App with slug '" + slug + "' already exists in this environment");
        }
        return appRepo.create(environmentId, slug, displayName);
    }

    public AppVersion uploadJar(UUID appId, String filename, InputStream jarData, long size) throws IOException {
        getById(appId); // verify app exists
        int nextVersion = versionRepo.findMaxVersion(appId) + 1;
        ArtifactCoordinates coords = new ArtifactCoordinates(tenantId, appId, nextVersion);

        // Buffer once for hashing + storage so RuntimeDetector can re-read without seeking.
        ByteArrayOutputStream buf = new ByteArrayOutputStream(
                Math.max(8192, (int) Math.min(size, 64 * 1024 * 1024)));
        MessageDigest digest;
        try { digest = MessageDigest.getInstance("SHA-256"); }
        catch (Exception e) { throw new RuntimeException(e); }
        try (InputStream in = jarData) {
            byte[] tmp = new byte[8192];
            int n;
            while ((n = in.read(tmp)) != -1) {
                buf.write(tmp, 0, n);
                digest.update(tmp, 0, n);
            }
        }
        byte[] bytes = buf.toByteArray();

        String locator = artifactStore.put(coords, new ByteArrayInputStream(bytes), bytes.length);
        String checksum = HexFormat.of().formatHex(digest.digest());

        UUID versionId = versionRepo.create(appId, nextVersion, locator, checksum, filename, size);

        // RuntimeDetector currently takes a Path; write bytes to a scratch file just for detection.
        java.nio.file.Path scratch = java.nio.file.Files.createTempFile("cameleer-detect-", ".jar");
        try {
            java.nio.file.Files.write(scratch, bytes);
            RuntimeDetector.DetectionResult detection = RuntimeDetector.detect(scratch);
            if (detection.runtimeType() != null) {
                versionRepo.updateDetectedRuntime(versionId, detection.runtimeType().toConfigValue(), detection.mainClass());
                log.info("Uploaded JAR for app {}: version={}, size={}, sha256={}, detected={}",
                        appId, nextVersion, size, checksum, detection.runtimeType().toConfigValue());
            } else {
                log.info("Uploaded JAR for app {}: version={}, size={}, sha256={}, detected=unknown",
                        appId, nextVersion, size, checksum);
            }
        } finally {
            try { java.nio.file.Files.deleteIfExists(scratch); } catch (IOException ignored) {}
        }

        return versionRepo.findById(versionId).orElseThrow();
    }

    public void deleteApp(UUID id) {
        appRepo.delete(id);
    }
}
