package ai.decompile.workspace.service;

import ai.decompile.analysis.event.StaticAnalysisRequestedEvent;
import ai.decompile.common.exception.NotFoundException;
import ai.decompile.workspace.event.BinaryUploadedEvent;
import ai.decompile.workspace.model.dto.BinaryDependencyResponse;
import ai.decompile.workspace.model.dto.BinaryResponse;
import ai.decompile.workspace.model.entity.Binary;
import ai.decompile.workspace.model.entity.BinaryDependency;
import ai.decompile.workspace.model.repository.BinaryDependencyRepository;
import ai.decompile.workspace.model.repository.BinaryRepository;
import ai.decompile.workspace.model.specification.BinarySpecification;
import ai.decompile.workspace.service.storage.FileResource;
import ai.decompile.workspace.service.storage.FileStorage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
@RequiredArgsConstructor
public class BinaryService {
  private final ProjectService projectService;
  private final BinaryRepository binaryRepository;
  private final BinaryDependencyRepository dependencyRepository;
  private final FileStorage fileStorage;
  private final ApplicationEventPublisher eventPublisher;

  public BinaryResponse getBinaryById(UUID binaryId) {
    return BinaryResponse.from(getBinary(binaryId));
  }

  public List<BinaryResponse> getBinaries(UUID projectId, String filename) {
    projectService.getProject(projectId);

    Specification<Binary> spec = BinarySpecification.hasProjectId(projectId);
    if (StringUtils.isNotBlank(filename)) {
      spec = spec.and(BinarySpecification.filenameContains(filename));
    }

    return binaryRepository.findAll(spec).stream().map(BinaryResponse::from).toList();
  }

  @Transactional
  public BinaryResponse uploadBinary(
      UUID projectId, MultipartFile file, String engine, List<MultipartFile> dependencies) {
    var project = projectService.getProject(projectId);

    String storedPath;
    try {
      storedPath = fileStorage.store(file.getInputStream(), file.getOriginalFilename());
    } catch (IOException e) {
      throw new RuntimeException("Failed to store file", e);
    }

    var binary =
        binaryRepository.save(
            Binary.builder()
                .filename(file.getOriginalFilename())
                .fileSize(file.getSize())
                .filePath(storedPath)
                .project(project)
                .build());

    if (Objects.nonNull(dependencies) && !dependencies.isEmpty()) {
      storeDependencies(binary, dependencies);
    }

    eventPublisher.publishEvent(
        new BinaryUploadedEvent(
            binary.getId(),
            binary.getProject().getId(),
            binary.getProject().getWorkspace().getId()));

    if (engine != null && !engine.isBlank()) {
      eventPublisher.publishEvent(
          new StaticAnalysisRequestedEvent(
              binary.getId(),
              binary.getProject().getId(),
              binary.getProject().getWorkspace().getId(),
              engine.strip().toUpperCase()));
    }

    return BinaryResponse.from(binary);
  }

  @Transactional
  public void requestStaticAnalysis(UUID binaryId, String engine) {
    var binary = getBinary(binaryId);
    eventPublisher.publishEvent(
        new StaticAnalysisRequestedEvent(
            binary.getId(),
            binary.getProject().getId(),
            binary.getProject().getWorkspace().getId(),
            engine.strip().toUpperCase()));
  }

  @Transactional
  public void updateAnalysisInfo(
      UUID binaryId, String format, String architecture, String compiler) {
    var binary = getBinary(binaryId);
    binary.setFormat(format);
    binary.setArchitecture(architecture);
    binary.setCompiler(compiler);
    binaryRepository.save(binary);
  }

  @Transactional
  public void deleteBinary(UUID binaryId) {
    var binary = getBinary(binaryId);

    for (var dep : binary.getDependencies()) {
      try {
        fileStorage.delete(dep.getFilePath());
      } catch (IOException e) {
        throw new RuntimeException("Failed to delete dependency file", e);
      }
    }

    try {
      fileStorage.delete(binary.getFilePath());
    } catch (IOException e) {
      throw new RuntimeException("Failed to delete file", e);
    }

    binaryRepository.delete(binary);
  }

  @Transactional
  public List<BinaryDependencyResponse> uploadDependencies(
      UUID binaryId, List<MultipartFile> files) {
    var binary = getBinary(binaryId);
    var deps = storeDependencies(binary, files);
    return deps.stream().map(BinaryDependencyResponse::from).toList();
  }

  public List<BinaryDependencyResponse> getDependencies(UUID binaryId) {
    var binary = getBinary(binaryId);
    return binary.getDependencies().stream().map(BinaryDependencyResponse::from).toList();
  }

  @Transactional
  public void deleteDependency(UUID binaryId, UUID dependencyId) {
    var binary = getBinary(binaryId);
    var dep =
        binary.getDependencies().stream()
            .filter(d -> d.getId().equals(dependencyId))
            .findFirst()
            .orElseThrow(
                () ->
                    new NotFoundException(
                        "Dependency with id '"
                            + dependencyId
                            + "' not found for binary '"
                            + binaryId
                            + "'"));

    try {
      fileStorage.delete(dep.getFilePath());
    } catch (IOException e) {
      throw new RuntimeException("Failed to delete dependency file", e);
    }

    binary.getDependencies().remove(dep);
    dependencyRepository.delete(dep);
  }

  public List<String> getDependencyFilePaths(UUID binaryId) {
    return getBinary(binaryId).getDependencies().stream()
        .map(dep -> fileStorage.resolvePath(dep.getFilePath()))
        .toList();
  }

  public FileResource downloadBinary(UUID binaryId) {
    var binary = getBinary(binaryId);
    try {
      return new FileResource(
          fileStorage.read(binary.getFilePath()), binary.getFilename(), binary.getFileSize());
    } catch (IOException e) {
      throw new RuntimeException("Failed to read file", e);
    }
  }

  @Transactional
  public void markStaticAnalysisDone(UUID binaryId) {
    var binary = getBinary(binaryId);
    binary.setStaticAnalysisDone(true);
    binaryRepository.save(binary);
  }

  public String getBinaryFilePath(UUID binaryId) {
    var relativePath = getBinary(binaryId).getFilePath();
    return fileStorage.resolvePath(relativePath);
  }

  public Binary getBinary(UUID binaryId) {
    return binaryRepository
        .findById(binaryId)
        .orElseThrow(() -> new NotFoundException("Binary with id '" + binaryId + "' not found"));
  }

  private List<BinaryDependency> storeDependencies(Binary binary, List<MultipartFile> files) {
    var deps = new ArrayList<BinaryDependency>();
    for (var file : files) {
      String storedPath;
      try {
        storedPath = fileStorage.store(file.getInputStream(), file.getOriginalFilename());
      } catch (IOException e) {
        throw new RuntimeException("Failed to store dependency file", e);
      }

      deps.add(
          dependencyRepository.save(
              BinaryDependency.builder()
                  .binary(binary)
                  .filename(file.getOriginalFilename())
                  .fileSize(file.getSize())
                  .filePath(storedPath)
                  .build()));
    }

    return deps;
  }
}
