package ai.decompile.analysis.service;

import ai.decompile.analysis.model.dto.LibraryResponse;
import ai.decompile.analysis.model.dto.LibrarySymbolResponse;
import ai.decompile.analysis.model.entity.Library;
import ai.decompile.analysis.model.entity.LibraryAlias;
import ai.decompile.analysis.model.entity.LibrarySymbol;
import ai.decompile.analysis.model.repository.LibraryAliasRepository;
import ai.decompile.analysis.model.repository.LibraryRepository;
import ai.decompile.analysis.model.repository.LibrarySymbolRepository;
import ai.decompile.analysis.service.parser.LibrarySymbolParserRegistry;
import ai.decompile.common.exception.NotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Log4j2
@Service
@RequiredArgsConstructor
public class LibraryService {
  private static final int SYMBOL_BATCH_SIZE = 500;

  private final LibraryRepository libraryRepository;
  private final LibraryAliasRepository aliasRepository;
  private final LibrarySymbolRepository symbolRepository;
  private final LibrarySymbolParserRegistry parserRegistry;

  @Transactional
  public LibraryResponse parseAndStore(
      String name, String description, String parserType, String aliases, MultipartFile file) {
    var parser = parserRegistry.get(parserType);

    var library =
        libraryRepository.save(Library.builder().name(name).description(description).build());

    var aliasList = parseAliasList(aliases);
    for (var alias : aliasList) {
      aliasRepository.save(LibraryAlias.builder().library(library).alias(alias).build());
    }

    var rawContent = readFileContent(file);
    var symbols = parser.parse(rawContent);
    var batch = new ArrayList<LibrarySymbol>(SYMBOL_BATCH_SIZE);

    for (var parsed : symbols) {
      batch.add(
          LibrarySymbol.builder()
              .library(library)
              .moduleName(parsed.moduleName())
              .className(parsed.className())
              .symbolName(parsed.symbolName())
              .ordinal(parsed.ordinal())
              .symbolType(parsed.symbolType())
              .parameters(parsed.parameters())
              .category(parsed.category())
              .build());

      if (batch.size() >= SYMBOL_BATCH_SIZE) {
        symbolRepository.saveAll(batch);
        batch.clear();
      }
    }

    if (!batch.isEmpty()) {
      symbolRepository.saveAll(batch);
    }

    log.info(
        "Library '{}' created with {} symbols and {} aliases",
        name,
        symbols.size(),
        aliasList.size());

    return LibraryResponse.from(library);
  }

  @Transactional(readOnly = true)
  public List<LibraryResponse> listLibraries() {
    return libraryRepository.findAll().stream().map(LibraryResponse::from).toList();
  }

  @Transactional(readOnly = true)
  public LibraryResponse getLibrary(UUID id) {
    return LibraryResponse.from(findLibrary(id));
  }

  @Transactional
  public void deleteLibrary(UUID id) {
    var library = findLibrary(id);
    libraryRepository.delete(library);
    log.info("Library '{}' deleted", library.getName());
  }

  @Transactional(readOnly = true)
  public Page<LibrarySymbolResponse> listSymbols(UUID libraryId, Pageable pageable) {
    return symbolRepository
        .findByLibraryIdOrderByOrdinalAsc(libraryId, pageable)
        .map(LibrarySymbolResponse::from);
  }

  @Transactional(readOnly = true)
  public LibrarySymbolResponse getSymbol(UUID id) {
    return LibrarySymbolResponse.from(findSymbol(id));
  }

  @Transactional(readOnly = true)
  public List<String> getAvailableParsers() {
    return parserRegistry.getAvailableFormats();
  }

  private String readFileContent(MultipartFile file) {
    try {
      return new String(file.getBytes(), StandardCharsets.UTF_8);
    } catch (IOException e) {
      throw new RuntimeException("Failed to read uploaded library dump file", e);
    }
  }

  private List<String> parseAliasList(String aliases) {
    if (Objects.isNull(aliases) || aliases.isBlank()) {
      return List.of();
    }

    return Stream.of(aliases.split(",")).map(String::trim).filter(a -> !a.isEmpty()).toList();
  }

  Library findLibrary(UUID id) {
    return libraryRepository
        .findById(id)
        .orElseThrow(() -> new NotFoundException("Library with id '" + id + "' not found"));
  }

  LibrarySymbol findSymbol(UUID id) {
    return symbolRepository
        .findById(id)
        .orElseThrow(() -> new NotFoundException("Library symbol with id '" + id + "' not found"));
  }
}
