package de.ph87.isabell_und_timo.picture;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.mortennobel.imagescaling.DimensionConstrain;
import com.mortennobel.imagescaling.MultiStepRescaleOp;
import jakarta.annotation.Nullable;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.util.unit.DataSize;
import org.springframework.web.bind.annotation.CrossOrigin;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
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 javax.imageio.ImageIO;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import static de.ph87.isabell_und_timo.picture.CONFIG.PREVIEW_SIZE;

@Slf4j
@CrossOrigin
@RestController
@RequiredArgsConstructor
@RequestMapping("Picture")
public class PictureController {

  private final PictureService pictureService;

  private final PictureRepository pictureRepository;

  @Bean
  public MultipartConfigElement multipartConfigElement() {
    final MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setMaxFileSize(DataSize.ofMegabytes(15));
    factory.setMaxRequestSize(DataSize.ofMegabytes(15));
    return factory.createMultipartConfig();
  }

  private static boolean isAdmin(@Nullable final String password) {
    return password != null && password.equals("IbPAadstIT25!");
  }

  @NonNull
  @PostMapping("all")
  public List<PictureDto> all(@RequestBody(required = false) final String password) {
    if (isAdmin(password)) {
      return pictureRepository.findAllDto();
    }
    return pictureRepository.findAllDtoByVisibleTrue();
  }

  @NonNull
  @PostMapping("{uuid}/visible/{visible}")
  public List<PictureDto> visible(@PathVariable @NonNull final String uuid, @PathVariable final boolean visible, @RequestBody(required = false) final String password) {
    if (isAdmin(password)) {
      pictureService.visible(uuid, visible);
    }
    return all(password);
  }

  @GetMapping("{uuid}/preview")
  public void preview(@PathVariable @NonNull final String uuid, @NonNull final HttpServletRequest request, @NonNull final HttpServletResponse response) throws IOException {
    final PictureInternal pictureDto = pictureRepository.findDtoByUuid(uuid).orElseThrow(() -> {
      log.warn("Tried accessing NON-EXISTENT picture: {}", request);
      return new ResponseStatusException(HttpStatus.NOT_FOUND);
    });

    final int cacheDuration = 30 * 24 * 60 * 60;
    response.setHeader("Cache-Control", "public, max-age=" + cacheDuration);
    response.setDateHeader("Expires", System.currentTimeMillis() + (cacheDuration * 1000L));
    response.setDateHeader("Last-Modified", System.currentTimeMillis());

    try (final FileInputStream input = new FileInputStream(pictureDto.getPreviewPath().toFile())) {
      response.getOutputStream().write(input.readAllBytes());
    }
  }

  @PostMapping(value = "upload")
  public void uploadImage(
    @NonNull @RequestParam("file") final MultipartFile fileStream,
    @NonNull final HttpServletRequest request,
    @Nullable @RequestHeader(value = "User-Agent", required = false) final String userAgent
  ) throws IOException, ImageProcessingException {
    if (fileStream.isEmpty()) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing 'file'");
    }

    final String originalFilename = fileStream.getOriginalFilename() == null ? "" : fileStream.getOriginalFilename();
    final int dotIndex = originalFilename.lastIndexOf('.');
    final String extension = (dotIndex >= 0) ? originalFilename.substring(dotIndex + 1) : "jpg";
    final String baseName = (dotIndex >= 0) ? originalFilename.substring(0, dotIndex) : originalFilename;
    final ZonedDateTime uploadDate = ZonedDateTime.now();
    final String filename = "%s_%s.%s".formatted(uploadDate.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), baseName, extension);

    final ByteArrayInputStream buffer = new ByteArrayInputStream(fileStream.getBytes());
    BufferedImage originalImage = ImageIO.read(buffer);
    if (originalImage == null) {
      throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Not a valid image");
    }

    buffer.reset();
    final Metadata metadata = ImageMetadataReader.readMetadata(buffer);
    final ExifSubIFDDirectory exif = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
    final ZonedDateTime date = exif != null && exif.getDateOriginal() != null ? ZonedDateTime.ofInstant(exif.getDateOriginal().toInstant(), ZoneId.systemDefault()) : null;
    final ExifIFD0Directory ifd0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
    final String make = ifd0 != null ? ifd0.getString(ExifIFD0Directory.TAG_MAKE) : null;
    final String model = ifd0 != null ? ifd0.getString(ExifIFD0Directory.TAG_MODEL) : null;

    try {
      if (ifd0 != null && ifd0.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
        final int orientation = ifd0.getInt(ExifIFD0Directory.TAG_ORIENTATION);
        originalImage = transformImageByOrientation(originalImage, orientation);
      }
    } catch (MetadataException e) {
      log.error("Failed to read orientation: filename={}, error={}", filename, e.getMessage());
    }

    final PictureInternal pictureDto = pictureService.create(date, make, model, originalImage.getWidth(), originalImage.getHeight(), fileStream.getSize(), originalFilename, filename, uploadDate, request.getRemoteAddr(), userAgent);
    final Path originalPath = pictureDto.getOriginalPath();
    final Path previewPath = pictureDto.getPreviewPath();

    mkdir(originalPath.getParent());
    mkdir(previewPath.getParent());

    buffer.reset();
    Files.copy(buffer, originalPath);

    final MultiStepRescaleOp rescale = new MultiStepRescaleOp(DimensionConstrain.createMaxDimension(PREVIEW_SIZE, PREVIEW_SIZE));
    final BufferedImage previewImage = rescale.filter(originalImage, null);
    ImageIO.write(previewImage, extension, previewPath.toFile());
  }

  private static void mkdir(@NonNull final Path directory) {
    if (directory.toFile().mkdirs()) {
      log.info("Directory created: {}", directory);
    }
  }

  private static BufferedImage transformImageByOrientation(BufferedImage image, int orientation) {
    int w = image.getWidth();
    int h = image.getHeight();
    final AffineTransform tx = new AffineTransform();

    switch (orientation) {
      case 1:
        return image;
      case 2: // Spiegeln horizontal
        tx.scale(-1, 1);
        tx.translate(-w, 0);
        break;
      case 3: // 180°
        tx.translate(w, h);
        tx.rotate(Math.toRadians(180));
        break;
      case 4: // Spiegeln vertikal
        tx.scale(1, -1);
        tx.translate(0, -h);
        break;
      case 5: // 90° + Spiegelung horizontal
        tx.translate(h, 0);
        tx.rotate(Math.toRadians(90));
        tx.scale(-1, 1);
        break;
      case 6: // 90°
        tx.translate(h, 0);
        tx.rotate(Math.toRadians(90));
        break;
      case 7: // 270° + Spiegelung horizontal
        tx.translate(0, w);
        tx.rotate(Math.toRadians(270));
        tx.scale(-1, 1);
        break;
      case 8: // 270°
        tx.translate(0, w);
        tx.rotate(Math.toRadians(270));
        break;
      default:
        return image;
    }

    // Falls rotiert: Breite/Höhe ggf. tauschen
    boolean swapDim = (orientation == 5 || orientation == 6 || orientation == 7 || orientation == 8);
    int newW = swapDim ? h : w;
    int newH = swapDim ? w : h;

    // Transformation anwenden
    final AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
    final BufferedImage transformed = new BufferedImage(newW, newH, image.getType());
    op.filter(image, transformed);
    return transformed;
  }

}
