package auctiora;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import lombok.extern.slf4j.Slf4j;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.dnn.Dnn;
import org.opencv.dnn.Net;
import org.opencv.imgcodecs.Imgcodecs;
import java.io.Console;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import static org.opencv.dnn.Dnn.DNN_BACKEND_OPENCV;
import static org.opencv.dnn.Dnn.DNN_TARGET_CPU;
/**
 * Service for performing object detection on images using OpenCV's DNN
 * module.  The DNN module can load pre‑trained models from several
 * frameworks (Darknet, TensorFlow, ONNX, etc.)【784097309529506†L209-L233】.  Here
 * we load a YOLO model (Darknet) by specifying the configuration and
 * weights files.  For each image we run a forward pass and return a
 * list of detected class labels.
 *
 * If model files are not found, the service operates in disabled mode
 * and returns empty lists.
 */
@Slf4j
public class ObjectDetectionService {

   private final Net          net;
   private final List<String> classNames;
   private final boolean      enabled;

   ObjectDetectionService(String cfgPath, String weightsPath, String classNamesPath) throws IOException {
      // Check if model files exist
      var cfgFile        = Paths.get(cfgPath);
      var weightsFile    = Paths.get(weightsPath);
      var classNamesFile = Paths.get(classNamesPath);
      
      if (!Files.exists(cfgFile) || !Files.exists(weightsFile) || !Files.exists(classNamesFile)) {
         log.info("⚠️  Object detection disabled: YOLO model files not found");
         log.info("   Expected files:");
         log.info("   - {}", cfgPath);
         log.info("   - {}", weightsPath);
         log.info("   - {}", classNamesPath);
         log.info("   Scraper will continue without image analysis.");
         this.enabled    = false;
         this.net        = null;
         this.classNames = new ArrayList<>();
         return;
      }
      
      try {
         // Load network
         this.net = Dnn.readNetFromDarknet(cfgPath, weightsPath);
         this.net.setPreferableBackend(DNN_BACKEND_OPENCV);
         this.net.setPreferableTarget(DNN_TARGET_CPU);
         // Load class names (one per line)
         this.classNames = Files.readAllLines(classNamesFile);
         this.enabled    = true;
         log.info("✓ Object detection enabled with YOLO");
      } catch (Exception e) {
         System.err.println("⚠️  Object detection disabled: " + e.getMessage());
         throw new IOException("Failed to initialize object detection", e);
      }
   }
   /**
    * Detects objects in the given image file and returns a list of
    * human‑readable labels.  Only detections above a confidence
    * threshold are returned.  For brevity this method omits drawing
    * bounding boxes.  See the OpenCV DNN documentation for details on
    * post‑processing【784097309529506†L324-L344】.
    *
    * @param imagePath absolute path to the image
    * @return list of detected class names (empty if detection disabled)
    */
   List<String> detectObjects(String imagePath) {
      if (!enabled) {
         return new ArrayList<>();
      }
      
      List<String> labels = new ArrayList<>();
      var          image  = Imgcodecs.imread(imagePath);
      if (image.empty()) return labels;
      // Create a 4D blob from the image
      var blob = Dnn.blobFromImage(image, 1.0 / 255.0, new Size(416, 416), new Scalar(0, 0, 0), true, false);
      net.setInput(blob);
      List<Mat> outs     = new ArrayList<>();
      var       outNames = getOutputLayerNames(net);
      net.forward(outs, outNames);
      // Post‑process: for each detection compute score and choose class
      var confThreshold = 0.5f;
      for (var out : outs) {
         for (var i = 0; i < out.rows(); i++) {
            var data = out.get(i, 0);
            if (data == null) continue;
            // The first 5 numbers are bounding box, then class scores
            var scores = new double[classNames.size()];
            System.arraycopy(data, 5, scores, 0, scores.length);
            var classId    = argMax(scores);
            var confidence = scores[classId];
            if (confidence > confThreshold) {
               var label = classNames.get(classId);
               if (!labels.contains(label)) {
                  labels.add(label);
               }
            }
         }
      }
      return labels;
   }
   /**
    * Returns the indexes of the output layers in the network.  YOLO
    * automatically discovers its output layers; other models may require
    * manually specifying them【784097309529506†L356-L365】.
    */
   private List<String> getOutputLayerNames(Net net) {
      List<String> names       = new ArrayList<>();
      var          outLayers   = net.getUnconnectedOutLayers().toList();
      var          layersNames = net.getLayerNames();
      for (var i : outLayers) {
         names.add(layersNames.get(i - 1));
      }
      return names;
   }
   /**
    * Returns the index of the maximum value in the array.
    */
   private int argMax(double[] array) {
      var best = 0;
      var max  = array[0];
      for (var i = 1; i < array.length; i++) {
         if (array[i] > max) {
            max  = array[i];
            best = i;
         }
      }
      return best;
   }
}
