package com.theorchard.javaowsassetbulkscript.logic;

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.stream.Collectors;

import javax.imageio.ImageIO;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.ResponseEntity;

import com.theorchard.javaowsassetbulkscript.ImportedProductRepository;
import com.theorchard.javaowsassetbulkscript.UnknownOrchardUpcRepository;
import com.theorchard.javaowsassetbulkscript.api.ApiConnector;
import com.theorchard.javaowsassetbulkscript.api.ProductDocument;
import com.theorchard.javaowsassetbulkscript.api.ProductItem;
import com.theorchard.javaowsassetbulkscript.api.ProductListing;
import com.theorchard.javaowsassetbulkscript.api.ProductStatus;
import com.theorchard.javaowsassetbulkscript.api.TrackItem;
import com.theorchard.javaowsassetbulkscript.api.TrackListing;
import com.theorchard.javaowsassetbulkscript.api.UploadToken;
import com.theorchard.javaowsassetbulkscript.models.ImportedProduct;
import com.theorchard.javaowsassetbulkscript.models.LabelStatus;
import com.theorchard.javaowsassetbulkscript.models.UnknownOrchardUpc;


public class AutomaticAssetImporter {

    private Logger logger = null;
    private Logger errLogger = null;

    private ImportedProductRepository importedProductRepository;
    private UnknownOrchardUpcRepository unknownOrchardUpcRepository;
    private LabelStatus labelStatus;

    private String status;

    private Map<Integer, String> productCodeMapping = null;

    public AutomaticAssetImporter() {
        this.importedProductRepository = null;
        this.unknownOrchardUpcRepository = null;
        this.labelStatus = null;
    }

	public AutomaticAssetImporter(ImportedProductRepository importedProductRepository,
			UnknownOrchardUpcRepository unknownOrchardUpcRepository, LabelStatus labelStatus) {
        this.importedProductRepository = importedProductRepository;
        this.unknownOrchardUpcRepository = unknownOrchardUpcRepository;
        this.labelStatus = labelStatus;
    }

    private void createLogger(String name) {
        logger = Logger.getLogger(name);
        errLogger = Logger.getLogger(name+"_err");

        try {
            FileHandler handler = new FileHandler("logs/"+name+".log");
            FileHandler errHandler = new FileHandler("logs/"+name+"_err.log");
            logger.addHandler(handler);
            errLogger.addHandler(errHandler);
            SimpleFormatter formatter = new SimpleFormatter();
            handler.setFormatter(formatter);
            errHandler.setFormatter(formatter);
        } catch (SecurityException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public boolean transferLabel(AWSDetails awsDetails, String labelId, int vendorId, boolean reuploadTimedout, boolean updateAssets) throws Exception {
        createLogger(labelId);

        AssetDownloader downloader = new AssetDownloader(awsDetails);

        boolean productListsIdentical = true;

        status = "";

        try {
            File metadata = downloader.downloadMetadataSpreadsheet(labelId);

            log("Transferring "+labelId + "("+vendorId+")");

            ProductListing listingLabelProcessing = ApiConnector.getProducts(vendorId);
            
            ProductListing listingInContent = ApiConnector.getProductsInContent(vendorId);

			int totalReleases = listingLabelProcessing.getItems().size() + listingInContent.getItems().size();
            
            log("Found " + totalReleases + " Release for Vendor " + vendorId);

            List<DeliveredProduct> products = LabelExcelLoader.getExpectedProductList(metadata);

            productListsIdentical = compareProductLists(products, listingLabelProcessing, listingInContent);

            logUnknownOrchardUpcsToDb(products, listingLabelProcessing, labelId, vendorId);
            
            log("Found " + products.size() + " Release(s) in the Metadata Sheet on the provided Bucket(" + awsDetails.getBucket() + ")");
            log("Start Transferring " + listingLabelProcessing.getItems().size() + " Release(s) for Vendor " + vendorId);

            for (DeliveredProduct product : products) {
            	transferProduct(product, awsDetails, listingLabelProcessing, listingInContent, vendorId, reuploadTimedout, updateAssets);
            	downloader.removeAlbumDownloadFolderForLabel(labelId, product.getId());
            }
            
            if (products.stream().filter(product -> product.getError()).count() > 0) {
                return false;
            }

        } catch (Exception e) {
            e.printStackTrace();
            logError(e.getMessage());
            throw e;
        } finally {
            downloader.removeAllDownloadsForLabel(labelId);
            downloader.close();
        }

        return productListsIdentical;
    }

    private void logUnknownOrchardUpcsToDb(List<DeliveredProduct> products, ProductListing listing, String labelId, int vendorId) {
        Set<Long> orchardUpcs = new HashSet<>();
        listing.getItems().stream().filter(productItem -> productItem.getContext_type().equals("digital")).forEach(productItem -> orchardUpcs.add(productItem.getUpc()));
		
        Set<Long> finetunesUpcs = new HashSet<>();
        for (DeliveredProduct product : products) {
        	Long upc = getFinetunesUpc(product);
        	if (upc == null) {
        		continue;
        	} else {
        		finetunesUpcs.add(Long.parseLong(product.getUpc()));
        	}
        }
        
        orchardUpcs.removeAll(finetunesUpcs);
        
        orchardUpcs.forEach(upc -> writeUpcToDb(upc, null, labelId, vendorId));
	}

	private void writeUpcToDb(Long upc, String finetunesUpc, String labelId, int vendorId) {
		UnknownOrchardUpc unknownOrchardUpc = unknownOrchardUpcRepository.findByUpc(upc);
		if (unknownOrchardUpc == null) {
			unknownOrchardUpc = new UnknownOrchardUpc(upc, finetunesUpc, labelId, vendorId);
		}

		unknownOrchardUpcRepository.save(unknownOrchardUpc);
	}

	private boolean compareProductLists(List<DeliveredProduct> deliveredProducts, ProductListing listingLabelProcessing, ProductListing listingInContent) {
        List<ProductItem> digitalProducts = new ArrayList<>();
        
        listingLabelProcessing.getItems().stream().filter(productItem -> "digital".equals(productItem.getContext_type())).forEach(productItem -> digitalProducts.add(productItem));

        listingInContent.getItems().stream().filter(productItem -> "digital".equals(productItem.getContext_type())).forEach(productItem -> digitalProducts.add(productItem));

        boolean itemMissing = false;

        for (DeliveredProduct product : deliveredProducts) {

        	boolean found = false;
        	for (ProductItem digitalProduct : digitalProducts) {

        	    try {
                    if (productCodeMatches(product, digitalProduct)) {
                        found = true;
                    }
                } catch (Exception e) {
                    logError("Could not get product codes for label " + product.getLabelId());
                }

            }

            if (!found) {
        	    itemMissing = true;
            }
        }

        if (digitalProducts.size() != deliveredProducts.size()) {
            status = "Product count mismatch";
            return false;
        }

        return !itemMissing;
    }

    private Long getFinetunesUpc(DeliveredProduct product) {
		long upc = -1L;
		try {
			upc = Long.parseLong(product.getUpc());
		} catch (NumberFormatException e) {
			logError("finetunes product " + product.getId() + " does not have a UPC!");
			return null;
		}
		return upc;
	}

	private boolean productIsNew(TrackListing trackListing, ProductStatus productStatus) throws Exception {
        for (TrackItem track : trackListing.getItems()) {
            int trackId = track.getTuid();
            if (productStatus.getStatus("audio", trackId).equals("new")) {
                return true;
            }
        }

        return (productStatus.getStatus("image",0).equals("new"));
    }

    private boolean productHasError(TrackListing trackListing, ProductStatus productStatus) throws Exception {
        for (TrackItem track : trackListing.getItems()) {
            int trackId = track.getTuid();
            if (productStatus.getStatus("audio", trackId).equals("error")) {
                return true;
            }
        }

        return (productStatus.getStatus("image",0).equals("error"));
    }

    private static boolean productIsFinished(TrackListing trackListing, ProductStatus productStatus) throws Exception {
        for (TrackItem track : trackListing.getItems()) {
            int trackId = track.getTuid();
            if (!productStatus.getStatus("audio", trackId).equals("finished")) {
                return false;
            }
        }

        return (productStatus.getStatus("image",0).equals("finished"));
    }

	private void transferProduct(DeliveredProduct product, AWSDetails awsDetails, ProductListing listingLabelProcessing, ProductListing listingInContent, int vendorId,
			boolean reuploadTimedout, boolean updateAssets) {
		
		boolean found = false;

        AssetDownloader downloader = null;

        int productId = 0;
		try {
			if (AssetImportController.aborted) {
				throw new Exception("Transfer for product " + product.getId() + ", "
						+ product.getUpc() + " aborted");
			}

			if (updateProductIfProductIsInContent(product, listingInContent)) {
			    return;
			}
			for (ProductItem productItem : listingLabelProcessing.getItems()) {

                if (productItem.getContext_type().equals("digital")
                        && productCodeMatches(product, productItem)) {

                    found = true;

                    productId = productItem.getProduct_id();

                    log("Found digital Product " + productId);
                    TrackListing trackListing = ApiConnector.getTracks(productId);
                    ProductStatus productStatus = getValidProductStatus(productId, trackListing);

                    if (reuploadTimedout || (!productIsFinished(trackListing, productStatus)
                            && !productIsNew(trackListing, productStatus) )) {
                        log("Found " + trackListing.getItems().size() + " Track(s)");

                        if (trackListing.getItems() == null
                                || trackListing.getItems().size() != product.getTracks().size()) {
                            throw new Exception(
                                    "Number of Tracks mismatch between " + product.getId() + " and " + productId);
                        }

                        log("Start transferring " + trackListing.getItems().size() + " Track(s)");

                        for (TrackItem trackItem : trackListing.getItems()) {


                            if (!productStatus.getStatus("audio", trackItem.getTuid()).equals("finished")
                                    || updateAssets) {
                                downloader = downloadAndTransferTrack(product, awsDetails, vendorId, trackItem);
                            }
                        }

                        if (!productStatus.getStatus("image", 0).equals("finished") || updateAssets) {
                            // we take the UPC from the ProductItem, because it may have been matched by the product code
                            downloader = downloadAndTransferCoverfile(product, awsDetails, vendorId, productId, String.valueOf(productItem.getUpc()));
                        }

                        waitForProductToFinish(productId);
                        updateProductStatus(productId, product.getId(), product.getLabelId(), product.getUpc(), "Good",
                                "Assets verified");

                    } else if (productIsNew(trackListing, productStatus)) {
                        waitForProductToFinish(productId);

                        updateProductStatus(productId, product.getId(), product.getLabelId(), product.getUpc(), "Good",
                                "Assets verified");
                    } else if (productIsFinished(trackListing, productStatus)) {
                        updateProductStatus(productId, product.getId(), product.getLabelId(), product.getUpc(), "Good",
                                "Assets verified");
                    }
                }
            }
		} catch (Exception e) {
			e.printStackTrace();
			logError(e.getMessage());
			updateProductStatus(productId, product.getId(), product.getLabelId(),
					product.getUpc(), "Error", e.getMessage());
			product.setError(true);
			return;
		} finally {
			if (downloader != null) {
				downloader.close();
			}
		}
		if (!found) {
			updateProductStatus(1, product.getId(), product.getLabelId(), product.getUpc(), "Error",
					"Product not found at The Orchard");
			product.setError(true);
		}
    }

	private boolean updateProductIfProductIsInContent(DeliveredProduct deliveredProduct, ProductListing listingInContent) {

		String finetunesId = deliveredProduct.getId();
		
		List<ProductItem> items = listingInContent.getItems();
		
		for (ProductItem item : items) {
			
			int orchardProductId = item.getProduct_id();
			
			long orchardUpc = item.getUpc();

            try {

                if (productCodeMatches(deliveredProduct, item)) {
                    updateProductStatus(orchardProductId, finetunesId, deliveredProduct.getLabelId(), String.valueOf(orchardUpc), "Good", "Assets verified");
                    return true;
                }
			} catch (Exception e) {
				logError("Error occured when matching product codes for product " + orchardProductId);
				updateProductStatus(orchardProductId, finetunesId, deliveredProduct.getLabelId(), String.valueOf(orchardUpc), "Error", e.getMessage());
			}
		}
		
		return false;
	}

	private boolean productCodeMatches(DeliveredProduct deliveredProduct, ProductItem productItem) throws Exception {
		int productItemProductId = productItem.getProduct_id();
		String productItemProductCode = getProductCode(productItemProductId);
		if (productItemProductCode == null) {
			return false;
		}
		boolean isMatch = productItemProductCode.equals(deliveredProduct.getProductCode());
		return isMatch;
	}

	private String getProductCode(int product_id) throws Exception {
		if (productCodeMapping == null) {
			productCodeMapping = new HashMap<>();
		}

    	if (productCodeMapping.containsKey(product_id)) {
    		return productCodeMapping.get(product_id);
		} else {
			ProductDocument productDocument = ApiConnector.getProductDocument(product_id);
			String product_code = productDocument.getProduct_code();
    		productCodeMapping.put(product_id, product_code);
		}

		return productCodeMapping.get(product_id);
	}

	private AssetDownloader downloadAndTransferCoverfile(DeliveredProduct product, AWSDetails awsDetails, int vendorId,
			int productId, String upc) throws Exception {
		AssetDownloader downloader;
		downloader = new AssetDownloader(awsDetails);
		try {
		    if (!downloader.exists(product.getCoverAsset())) {
		        throw new Exception ("File does not exist on the AWS bucket: "+product.getCoverAsset());
		    }

		    File cover = downloader.download(product.getCoverAsset());

		    BufferedImage bimg = ImageIO.read(cover);
		    int width          = bimg.getWidth();
		    int height         = bimg.getHeight();

		    if (width != height || width != 3000) {
		        BufferedImage resized = new BufferedImage(3000, 3000, bimg.getType());
		        Graphics2D g = resized.createGraphics();
		        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
		                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
		        g.drawImage(bimg, 0, 0, 3000, 3000, 0, 0, bimg.getWidth(),
		                bimg.getHeight(), null);
		        g.dispose();


		        File outputfile = new File(cover.getParent()+"/"+FilenameUtils.getBaseName(cover.getName())+"_3000.jpg");
		        ImageIO.write(resized, "jpg", outputfile);

		        cover = outputfile;
		    }

		    if (cover != null && cover.exists()) {
		        log("Transffering cover asset " + cover.getName());
		        
		        
		        uploadAsset(cover, productId, upc, 0, vendorId);
		    } else {
		        throw new Exception("Cover does not exist for " + product.getUpc());
		    }
		} catch (Exception e) {
		    throw e;
		} finally {
		    downloader.close();
		}
		return downloader;
	}

	private AssetDownloader downloadAndTransferTrack(DeliveredProduct product, AWSDetails awsDetails, int vendorId,
			TrackItem trackItem) throws Exception {
		AssetDownloader downloader;
		downloader = new AssetDownloader(awsDetails);
		try {
		    File trackFile = downloader.downloadProductTrack(product, trackItem);

		    if (trackFile == null || !trackFile.exists()) {
		        throw new Exception("Failed to download Track with ISRC " + trackItem.getIsrc());
		    }

		    DeliveredTrack track = product.getTrack(trackItem.getIsrc());

		    String md5 = DigestUtils.md5Hex(new FileInputStream(trackFile.getAbsolutePath()));

		    if (!md5.equals(track.getMd5())) {
		        throw new Exception("Md5 mismatch for Track with ISRC " + track.getIsrc() + " (" + trackFile.getName() + "): " + md5 + " - " + track.getMd5());
		    }
		    if (!transferTrack(trackItem, trackFile, product, vendorId)) {
		        throw new Exception("No valid File found for Track Item " + trackItem.getIsrc());
		    }
		} catch (Exception e) {
		    throw e;
		} finally {
		    downloader.close();
		}
		return downloader;
	}

    private static void validateProductStatus(ProductStatus productStatus, TrackListing trackListing) throws Exception {
        final List<String> validStatuses = Arrays.asList("new", "finished", "missing", "error");

        for (TrackItem track : trackListing.getItems()) {
            int trackId = track.getTuid();
            if (!validStatuses.contains(productStatus.getStatus("audio", trackId))) {
                throw new Exception("Status for audio asset for track "+trackId+" on product "+productStatus.getProduct_id()+" is invalid: "+productStatus.getStatus("audio", trackId));
            }
        }

        if (!validStatuses.contains(productStatus.getStatus("image",0))) {
            throw new Exception("Status for cover image on product "+productStatus.getProduct_id()+" is invalid: "+productStatus.getStatus("image", 0));
        }
    }

    public static boolean testProductIsDone(int productId) throws Exception {

        TrackListing trackListing = ApiConnector.getTracks(productId);
        ProductStatus productStatus = getValidProductStatus(productId, trackListing);

        return productIsFinished(trackListing, productStatus);
    }

    private void waitForProductToFinish(int productId) throws Exception {

        TrackListing trackListing = ApiConnector.getTracks(productId);
        ProductStatus productStatus = getValidProductStatus(productId, trackListing);

        int timeout = 600 * 1000;

        Long startTime = System.currentTimeMillis();

        Long timePassed = 0L;
        
        while (productIsNew(trackListing, productStatus)) {
            if (AssetImportController.aborted) {
                throw new Exception("Aborted while waiting for encoding");
            }

            log("Waiting for Product "+productStatus.getProduct_id()+" to finish: "+(timePassed / 1000)+"s");
            TimeUnit.SECONDS.sleep(5);
            trackListing = ApiConnector.getTracks(productId);
            productStatus = getValidProductStatus(productId, trackListing);

            if (productHasError(trackListing, productStatus)) {
                throw new Exception ("Error for assets");
            }

            timePassed = System.currentTimeMillis() - startTime;

            if (timePassed >= timeout) {
                throw new Exception("Waiting for product "+productId+" to finish timed out: "+(timeout / 1000)+" seconds surpassed");
            }
        }
    }

    private static ProductStatus getValidProductStatus(int productId, TrackListing trackListing) throws Exception {
        ProductStatus productStatus = ApiConnector.getProductStatus(productId);
        validateProductStatus(productStatus, trackListing);
        return productStatus;
    }

    private void updateProductStatus(int productId, String originalId, String labelId, String upc, String status, String message) {
        if (importedProductRepository != null) {
            log(productId+": "+message);
            ImportedProduct importedProduct = importedProductRepository.findOne(originalId);
            if (importedProduct == null) {
                log("Create new Product status for "+productId);

                importedProduct = new ImportedProduct(productId, originalId, labelStatus, upc);
            }
            importedProduct.setStatus(status);
            importedProduct.setMessage(message);
            importedProduct.setProductId(productId);
            importedProduct.setImportDate(new Date());

            importedProductRepository.save(importedProduct);
        }
    }

    private boolean transferTrack(TrackItem trackItem, File trackFile, DeliveredProduct product, int vendorId) throws Exception {
        for (DeliveredTrack ftTrack : product.getTracks()) {
            if (ftTrack.getIsrc().equals(trackItem.getIsrc())) {
//                if (ftTrack.getSet() != trackItem.getVolume_number() || ftTrack.getPos() != trackItem.getTrack_number()) {
//                    throw new Exception("Track Volume or Number mismatch for "+product.getUpc()+" - "+ftTrack.getIsrc());
//                } else {
                    if (trackFile.getName().equals(FilenameUtils.getName(ftTrack.getAsset()))) {
                        int productId = trackItem.getProduct_id();
                        String upc = trackItem.getUpc();
                        int trackId = trackItem.getTuid();

                        uploadAsset(trackFile, productId, upc, trackId, vendorId);

                        return true;
                    }
//                }
            }
        }
        return false;
    }

    private void createAsset(File trackFile, int productId, String upc, int trackId, int vendorId, UploadToken token) throws Exception {
        String type = FilenameUtils.getExtension(trackFile.getName());
        String uniqueFileName = token.getFilename()+"."+FilenameUtils.getExtension(trackFile.getName());

        Asset asset = new Asset(type, productId, upc, trackId, uniqueFileName, trackFile.getName(), token.getCredentials().getToken());

        ResponseEntity<String> response = ApiConnector.postAsset(asset, vendorId);

        log("Asset Creation Response: "+response.getBody());
    }

    private void uploadAsset(File asset, int productId, String upc, int trackId, int vendorId) throws Exception {
        UploadToken uploadToken = ApiConnector.getUploadToken(vendorId);

        log("Got Upload Token "+uploadToken.getCredentials().getToken() + " for Asset "+asset.getName());

        Credentials credentials = uploadToken.getCredentials();
        AWSWrapper wrapper = new AWSWrapper(credentials.getAws_access_key_id(), credentials.getAws_secret_access_key(), credentials.getToken(), uploadToken.getBucket(),"us-east-1");
        wrapper.connect();

        try {
            log("Uploading "+asset.getName()+" to "+uploadToken.getFilename() + "." + FilenameUtils.getExtension(asset.getName()));
            wrapper.uploadToDir(asset, "", uploadToken.getFilename() + "." + FilenameUtils.getExtension(asset.getName()));
        } catch (Exception e) {
            logError("Upload failed for "+asset.getName()+": "+e.getMessage());
            throw e;
        }

        try {
            wrapper.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        createAsset(asset, productId, upc, trackId, vendorId, uploadToken);
    }

    private void logError(String error) {
        errLogger.log(Level.SEVERE, error);
        logger.log(Level.SEVERE, error);
        System.err.println(error);
    }

    private void log(String message) {
        logger.log(Level.INFO, message);
        System.out.println(message);
    }

    public String getStatus() {
        return status;
    }
}
