package com.cameleer.server.app.controller;

import com.cameleer.common.model.ApplicationConfig;
import com.cameleer.server.app.dto.DirtyStateResponse;
import com.cameleer.server.app.storage.PostgresApplicationConfigRepository;
import com.cameleer.server.app.storage.PostgresDeploymentRepository;
import com.cameleer.server.app.web.EnvPath;
import com.cameleer.server.core.runtime.App;
import com.cameleer.server.core.runtime.AppService;
import com.cameleer.server.core.runtime.AppVersion;
import com.cameleer.server.core.runtime.AppVersionRepository;
import com.cameleer.server.core.runtime.Deployment;
import com.cameleer.server.core.runtime.DeploymentConfigSnapshot;
import com.cameleer.server.core.runtime.DirtyStateCalculator;
import com.cameleer.server.core.runtime.DirtyStateResult;
import com.cameleer.server.core.runtime.Environment;
import com.cameleer.server.core.runtime.RuntimeType;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * App CRUD and JAR upload. All routes env-scoped: the (env, appSlug) pair
 * identifies a single app — the same app slug can legitimately exist in
 * multiple environments with independent configuration and history.
 */
@RestController
@RequestMapping("/api/v1/environments/{envSlug}/apps")
@Tag(name = "App Management", description = "Application lifecycle and JAR uploads (env-scoped)")
@PreAuthorize("hasAnyRole('OPERATOR', 'ADMIN')")
public class AppController {

    private final AppService appService;
    private final AppVersionRepository appVersionRepository;
    private final PostgresApplicationConfigRepository configRepository;
    private final PostgresDeploymentRepository deploymentRepository;
    private final DirtyStateCalculator dirtyCalc;

    public AppController(AppService appService,
                         AppVersionRepository appVersionRepository,
                         PostgresApplicationConfigRepository configRepository,
                         PostgresDeploymentRepository deploymentRepository,
                         DirtyStateCalculator dirtyCalc) {
        this.appService = appService;
        this.appVersionRepository = appVersionRepository;
        this.configRepository = configRepository;
        this.deploymentRepository = deploymentRepository;
        this.dirtyCalc = dirtyCalc;
    }

    @GetMapping
    @Operation(summary = "List apps in this environment")
    @ApiResponse(responseCode = "200", description = "App list returned")
    public ResponseEntity<List<App>> listApps(@EnvPath Environment env) {
        return ResponseEntity.ok(appService.listByEnvironment(env.id()));
    }

    @GetMapping("/{appSlug}")
    @Operation(summary = "Get app by env + slug")
    @ApiResponse(responseCode = "200", description = "App found")
    @ApiResponse(responseCode = "404", description = "App not found in this environment")
    public ResponseEntity<App> getApp(@EnvPath Environment env, @PathVariable String appSlug) {
        try {
            return ResponseEntity.ok(appService.getByEnvironmentAndSlug(env.id(), appSlug));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @PostMapping
    @Operation(summary = "Create a new app in this environment",
            description = "Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. "
                    + "Slug is immutable after creation.")
    @ApiResponse(responseCode = "201", description = "App created")
    @ApiResponse(responseCode = "400", description = "Invalid slug, or slug already exists in this environment")
    public ResponseEntity<?> createApp(@EnvPath Environment env, @RequestBody CreateAppRequest request) {
        try {
            UUID id = appService.createApp(env.id(), request.slug(), request.displayName());
            return ResponseEntity.status(201).body(appService.getById(id));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        }
    }

    @GetMapping("/{appSlug}/versions")
    @Operation(summary = "List versions for this app")
    @ApiResponse(responseCode = "200", description = "Version list returned")
    public ResponseEntity<List<AppVersion>> listVersions(@EnvPath Environment env, @PathVariable String appSlug) {
        try {
            App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
            return ResponseEntity.ok(appService.listVersions(app.id()));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @PostMapping(value = "/{appSlug}/versions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "Upload a JAR for a new version of this app")
    @ApiResponse(responseCode = "201", description = "JAR uploaded and version created")
    @ApiResponse(responseCode = "404", description = "App not found in this environment")
    public ResponseEntity<AppVersion> uploadJar(@EnvPath Environment env,
                                                 @PathVariable String appSlug,
                                                 @RequestParam("file") MultipartFile file) throws IOException {
        try {
            App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
            AppVersion version = appService.uploadJar(app.id(), file.getOriginalFilename(), file.getInputStream(), file.getSize());
            return ResponseEntity.status(201).body(version);
        } catch (IllegalArgumentException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @DeleteMapping("/{appSlug}")
    @Operation(summary = "Delete this app")
    @ApiResponse(responseCode = "204", description = "App deleted")
    public ResponseEntity<Void> deleteApp(@EnvPath Environment env, @PathVariable String appSlug) {
        try {
            App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
            appService.deleteApp(app.id());
            return ResponseEntity.noContent().build();
        } catch (IllegalArgumentException e) {
            return ResponseEntity.notFound().build();
        }
    }

    @GetMapping("/{appSlug}/dirty-state")
    @Operation(summary = "Check whether the app's current config differs from the last successful deploy",
            description = "Returns dirty=true when the desired state (current JAR + agent config + container config) "
                    + "would produce a changed deployment. When no successful deploy exists yet, dirty=true.")
    @ApiResponse(responseCode = "200", description = "Dirty-state computed")
    @ApiResponse(responseCode = "404", description = "App not found in this environment")
    public ResponseEntity<DirtyStateResponse> getDirtyState(@EnvPath Environment env,
                                                             @PathVariable String appSlug) {
        App app;
        try {
            app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
        } catch (IllegalArgumentException e) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "App not found");
        }

        // Latest JAR version (newest first — findByAppId orders by version DESC)
        List<AppVersion> versions = appVersionRepository.findByAppId(app.id());
        UUID latestVersionId = versions.isEmpty() ? null
                : versions.stream().max(Comparator.comparingInt(AppVersion::version))
                          .map(AppVersion::id).orElse(null);

        // Desired agent config
        ApplicationConfig agentConfig = configRepository
                .findByApplicationAndEnvironment(appSlug, env.slug())
                .orElse(null);

        // Container config
        Map<String, Object> containerConfig = app.containerConfig();

        // Last successful deployment snapshot
        Deployment lastSuccessful = deploymentRepository
                .findLatestSuccessfulByAppAndEnv(app.id(), env.id())
                .orElse(null);
        DeploymentConfigSnapshot snapshot = lastSuccessful != null ? lastSuccessful.deployedConfigSnapshot() : null;

        DirtyStateResult result = dirtyCalc.compute(latestVersionId, agentConfig, containerConfig, snapshot);

        String lastId = lastSuccessful != null ? lastSuccessful.id().toString() : null;
        return ResponseEntity.ok(new DirtyStateResponse(result.dirty(), lastId, result.differences()));
    }

    private static final java.util.regex.Pattern CUSTOM_ARGS_PATTERN =
            java.util.regex.Pattern.compile("^[-a-zA-Z0-9_.=:/\\s+\"']*$");

    private void validateContainerConfig(Map<String, Object> config) {
        Object customArgs = config.get("customArgs");
        if (customArgs instanceof String s && !s.isBlank() && !CUSTOM_ARGS_PATTERN.matcher(s).matches()) {
            throw new IllegalArgumentException("customArgs contains invalid characters. Only JVM-style arguments are allowed.");
        }
        Object runtimeType = config.get("runtimeType");
        if (runtimeType instanceof String s && !s.isBlank() && RuntimeType.fromString(s) == null) {
            throw new IllegalArgumentException("Invalid runtimeType: " + s +
                    ". Must be one of: auto, spring-boot, quarkus, plain-java, native");
        }
    }

    @PutMapping("/{appSlug}/container-config")
    @Operation(summary = "Update container config for this app")
    @ApiResponse(responseCode = "200", description = "Container config updated")
    @ApiResponse(responseCode = "400", description = "Invalid configuration")
    @ApiResponse(responseCode = "404", description = "App not found in this environment")
    public ResponseEntity<?> updateContainerConfig(@EnvPath Environment env,
                                                    @PathVariable String appSlug,
                                                    @RequestBody Map<String, Object> containerConfig) {
        try {
            validateContainerConfig(containerConfig);
            App app = appService.getByEnvironmentAndSlug(env.id(), appSlug);
            appService.updateContainerConfig(app.id(), containerConfig);
            return ResponseEntity.ok(appService.getById(app.id()));
        } catch (IllegalArgumentException e) {
            if (e.getMessage().contains("not found")) {
                return ResponseEntity.notFound().build();
            }
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        }
    }

    public record CreateAppRequest(String slug, String displayName) {}
}
