package com.capgemini.reports.teamcenter;

import com.capgemini.framework.teamcenter.authent.TcSession;
import com.capgemini.framework.teamcenter.queries.saved.AbstractReplicatedQuery;
import com.capgemini.framework.teamcenter.queries.saved.modelobject.ItemRevisionQuery;
import com.capgemini.reports.exceptions.InvalidUserInputException;
import com.capgemini.reports.exceptions.NotFoundException;
import com.capgemini.reports.exceptions.UserInputErrorEnum;
import com.capgemini.reports.reports.parameters.EffectivityDate;
import com.capgemini.reports.reports.parameters.Generics;
import com.capgemini.reports.reports.parameters.ProductUnit;
import com.capgemini.reports.teamcenter.queries.DirectQuery;
import com.capgemini.reports.teamcenter.queries.saved.GenericAssembQuery;
import com.capgemini.reports.teamcenter.queries.saved.GenericQuery;
import com.capgemini.reports.teamcenter.queries.saved.ProductTypeQuery;
import com.capgemini.reports.teamcenter.queries.saved.UnitQuery;
import com.teamcenter.soa.client.model.ModelObject;
import com.teamcenter.soa.client.model.strong.*;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

import static com.capgemini.framework.teamcenter.TeamcenterObjectTypes.ITEM_REVISION;
import static com.capgemini.framework.teamcenter.object.SnecmaObjectProperties.PV_CLAUSE_PROPRIETE;
import static com.capgemini.framework.teamcenter.object.SnecmaObjectProperties.PV_TYPE_PRODUIT;
import static com.capgemini.framework.teamcenter.object.types.SnecmaObjectTypes.EFFECTIVITY;

/**
 * This class is able to send requests to the Teamcenter servers to obtain information that hasn't been loaded yet.<br>
 * Typical functionality is to fetch an object using its UID, load related documents or workflows, etc.
 */
@Slf4j
public class ObjectInfoLoader {

    private static final String TYPE = "Type : ";
    private static final String SEARCH_UNIT_OF_PRODUCT_ID = "search unit of product ID: ";
    private static final String SAVED_QUERY = "saved query : ";
    private static final String UNIT_REVISION_DOES_NOT_CONTAIN_EFFECTIVITIES = "Unit Revision does not contain Effectivities...";

    protected final TcSession session;

    /**
     * Constructs a {@link ObjectInfoLoader} with the {@link TcSession} to be used for the queries.
     *
     * @param session the {@link TcSession} to be used for the queries.
     */
    public ObjectInfoLoader(final TcSession session) {
        this.session = session;
    }

    /**
     * Requests the 'clause de propriete' on a product type.
     *
     * @param typeProduit the 'type produit' property
     * @param session     the Tc session to make the query
     * @return the clause de propriete
     * @throws NotFoundException if unable to find the form
     */
    public static String getClausePropriete(final String typeProduit, final TcSession session) throws NotFoundException {
        ProductTypeQuery theQuery = new ProductTypeQuery(session);
        theQuery.addNomTypeProduit(typeProduit.trim());
        List<Form> result = theQuery.findAll();
        for (Form form : result) {
            session.loadProperties(form, PV_CLAUSE_PROPRIETE);
            return form.getPropertyObject(PV_CLAUSE_PROPRIETE).getStringValue();
        }
        throw new NotFoundException("Could not find a Form with the clause de propriete");
    }

    /**
     * Returns the Product Type of a given ItemRevision.<br>
     *
     * @param itemRevision the item rev
     * @return the product type as a String
     */
    public static String getProductType(final TcSession session, final ItemRevision itemRevision) {
        // By rule, an itemRevision only owns a single Master Form Revision
        Form masterForm = itemRevision.get_item_master_tag();
        session.loadProperties(masterForm);
        return masterForm.getPropertyObject(PV_TYPE_PRODUIT).getStringValue();
    }

    private List<String> getUnits(final ReleaseStatus releaseStatus) throws NotFoundException {
        session.loadProperties(releaseStatus, "effectivities");
        ModelObject[] effectivities = releaseStatus.get_effectivities();
        session.loadProperties(effectivities);
        if (effectivities == null) {
            log.error(UNIT_REVISION_DOES_NOT_CONTAIN_EFFECTIVITIES);
            throw new NotFoundException(UNIT_REVISION_DOES_NOT_CONTAIN_EFFECTIVITIES);
        }

        List<String> listOfUnits = new ArrayList<>();
        for (ModelObject objectEffectivity : effectivities) {
            if (EFFECTIVITY.equals(releaseStatus.get_name())) {
                Effectivity effectivity = (Effectivity) objectEffectivity;
                String unitRange = effectivity.get_unit_range_text();
                log.debug("Unit range : " + unitRange);
                listOfUnits.add(unitRange);
            }
        }
        return listOfUnits;
    }

    /**
     * Renvoie une liste de Generique revision pour le produit dont l'ID est passe en parametre.
     *
     * @param productUnit le {@link ProductUnit}
     * @return List de Generic Ids
     * @throws NotFoundException if the Generic, the Product or the Unit could not be found
     */
    public List<String> getGenericsRevisionOfProduct(final ProductUnit productUnit) throws NotFoundException {
        return getGenericsRevisionOfProduct(productUnit.getProductID(), productUnit.getUnit());
    }

    /**
     * Renvoie une liste de Générique Montage revision pour le produit-unit est passé en paramètre.
     *
     * @param aProductUnit {@link ProductUnit} produit
     * @return List de GenericsAssemblies
     * @throws NotFoundException if the Generic Assemb, the Product or the Unit could not be found
     */
    public List<String> getGenericsAssembliesRevisionOfProduct(final ProductUnit aProductUnit) throws NotFoundException {
        return getGenericsAssembliesRevisionOfProduct(aProductUnit.getProductID(), aProductUnit.getUnit());
    }

    /**
     * Renvoie une liste de Generique revision pour le produit dont l'ID est passe en parametre.
     *
     * @param anObjectID Object ID du produit
     * @param unit       unit du produit
     * @return List de Generic IDs
     * @throws NotFoundException if the product could not be found
     */
    public List<String> getGenericsRevisionOfProduct(final String anObjectID, final int unit) throws NotFoundException {
        return getObjectRevisionOfProduct(new GenericQuery<ItemRevision>(session), anObjectID, unit);
    }

    /**
     * Renvoie une liste de Générique Montage revision pour le produit dont l'ID et le unitest passé en paramètre.
     *
     * @param aProductID Id du produit
     * @param unit       unit du produit
     * @return List de GenericsAssemblies
     * @throws NotFoundException if the Product could not be found
     */
    public List<String> getGenericsAssembliesRevisionOfProduct(final String aProductID, final int unit)
            throws NotFoundException {
        return getObjectRevisionOfProduct(new GenericAssembQuery<ItemRevision>(session), aProductID, unit);
    }

    /**
     * Common sub method for retrieving a list of generic or generic assemb.
     *
     * @return a {@link List} of revisions
     * @throws NotFoundException if the end item could not be found
     */
    private List<String> getObjectRevisionOfProduct(final AbstractReplicatedQuery<ItemRevision, ?> aQuery, final String anEndItemId, final int unit)
            throws NotFoundException {
        // Setup the query
        aQuery.addEndItemID(anEndItemId);
        aQuery.addEffectivityUnitIntro(Integer.toString(unit));

        // Obtain the list of Generics or Generics Assemb
        List<ItemRevision> result = aQuery.findAll();
        if (result.isEmpty()) {
            String msg = aQuery.getClass().getName() + " Could not find the request Item Revision...";
            log.error(msg);
            throw new NotFoundException(msg);
        }

        // Populate return list with the IDs
        List<String> listGenOrGenAssemb = new ArrayList<>();
        for (ItemRevision generic : result) {
            String itemId = generic.get_item_id();
            log.debug("{} Returned an object with ID : {}", aQuery.getClass().getName(), itemId);
            listGenOrGenAssemb.add(itemId);
        }

        return listGenOrGenAssemb;
    }

    /**
     * Renvoie une liste de Unit pour le produit dont l'ID est passé en paramêtre.
     *
     * @param productID "end_item_id" L'identifiant unique Teamcenter
     * @return Liste of Unit as {@link String}
     * @throws NotFoundException if no effectivities are found on the ReleaseStatus
     */
    public List<String> getUnitsOfProduct(final String productID) throws NotFoundException {
        // Setup the query
        UnitQuery unitQuery = new UnitQuery(session);
        unitQuery.addEndItemID(productID);

        // Perform the query: Obtain the list of all the UNIT Revision of that product
        List<ItemRevision> result = unitQuery.findAll();
        if (result.isEmpty()) {
            log.debug("No unit has been found using {} on product {}", unitQuery.getClass().getName(), productID);
            return new ArrayList<>();
        }

        // Parse the result to obtain the Units
        List<String> listOfUnits = new ArrayList<>();
        for (ItemRevision unitRevision : result) {
            session.loadProperties(unitRevision, "release_status_list");
            ReleaseStatus[] releaseStatuses = unitRevision.get_release_status_list();
            session.loadProperties(releaseStatuses);
            for (ReleaseStatus status : releaseStatuses) {
                listOfUnits.addAll(getUnits(status));
            }
        }
        return listOfUnits;
    }

    /**
     * Returns the ID of the object whose UID is provided.
     *
     * @param objectUID Lthe unique Teamcenter ID
     * @return the ID
     * @throws NotFoundException if the object does not point a Revision or Item
     */
    public String getObjectID(final String objectUID) throws NotFoundException {
        // Perform a query of the database
        ModelObject modelObject = new DirectQuery<>(session).searchByUID(objectUID);
        String itemID;
        if (modelObject.getTypeObject().isInstanceOf(ITEM_REVISION)) {
            ItemRevision queryItemRevision = (ItemRevision) modelObject;
            itemID = queryItemRevision.get_item_id();
        } else if (modelObject.getTypeObject().isInstanceOf("Item")) {
            Item queryItem = (Item) modelObject;
            itemID = queryItem.get_item_id();
        } else {
            String msg = "QueryObject is neither Item nor ItemRevision";
            log.error(msg);
            throw new NotFoundException(msg);
        }
        return itemID;
    }

    /**
     * Get {@link ItemRevision} of current object by uid.
     *
     * @param objectUID the (String) unique identifier
     * @return the {@link ItemRevision}
     * @throws NotFoundException         if the object could not be found
     * @throws InvalidUserInputException if If the uid does not point to a Tc object at all
     */
    public ItemRevision getItemRevisionByUID(final String objectUID) throws NotFoundException, InvalidUserInputException {
        // Obtain the Object
        ModelObject modelObject;
        try {
            modelObject = new DirectQuery<>(session).searchByUID(objectUID);
        } catch (NotFoundException e) {
            throw new NotFoundException("The provided UID could not be found", e);
        }

        if (!modelObject.getTypeObject().isInstanceOf(ITEM_REVISION)) {
            // The Object is not an ItemRevision!
            String msg = "QueryObject is not an ItemRevision";
            log.error(msg);
            throw new InvalidUserInputException(UserInputErrorEnum.OBJECT_DOES_NOT_EXIST);
        }
        return (ItemRevision) modelObject;
    }

    /**
     * Get generic revision by generic id, product id, effectivity date with saved query.
     *
     * @param produit         the {@link ProductUnit}
     * @param generic         the {@link Generics}
     * @param effectivityDate the {@link EffectivityDate}
     * @return listGeneric the list of Generic revisions for the input product/unit
     * @throws NotFoundException if the generic is not found
     */
    public List<String> getGenericsRevisionOfProductWithDate(final ProductUnit produit, final Generics generic, final EffectivityDate effectivityDate)
            throws NotFoundException {
        return getObjectRevisionOfProductWithDate(new GenericQuery<ItemRevision>(session), generic.getID(), produit.getProductID(),
                effectivityDate.getFormattedDateForTcRequest());
    }

    /**
     * Get generic assemb revision by generic id, product id, effectivity date with saved query.
     *
     * @param produit         the {@link ProductUnit}
     * @param generic         the {@link Generics}
     * @param effectivityDate the {@link EffectivityDate}
     * @return listGeneric the list of Generic Assemb revisions for the input product/unit
     * @throws NotFoundException if the generic assemb is not found
     */
    public List<String> getGenericsAssembRevisionOfProductWithDate(final ProductUnit produit, final Generics generic,
                                                                   final EffectivityDate effectivityDate) throws NotFoundException {
        return getObjectRevisionOfProductWithDate(new GenericAssembQuery<ItemRevision>(session), generic.getID(), produit.getProductID(),
                effectivityDate.getFormattedDateForTcRequest());
    }

    /**
     * Returns the Revisions of a Product.
     */
    private List<String> getObjectRevisionOfProductWithDate(final AbstractReplicatedQuery<ItemRevision, ?> aQuery, final String itemID,
                                                            final String anEndItemId, final String dateToday) throws NotFoundException {
        // Setup the query
        aQuery.searchID(itemID);
        aQuery.addEndItemID(anEndItemId);
        aQuery.addBeforeDate(dateToday);

        // Obtain the list of Generics or Generics Assemb
        List<ItemRevision> result = aQuery.findAll();
        if (result.isEmpty()) {
            String msg = aQuery.getClass().getName() + " Could not find the requeste ItemRevision...";
            log.error(msg);
            throw new NotFoundException(msg);
        }

        // Populate return list with the IDs
        List<String> listGenOrGenAssemb = new ArrayList<>();
        for (ItemRevision generic : result) {
            log.debug(TYPE + generic.getTypeObject().getName());
            String itemId = generic.get_item_id();
            log.debug("{} found an object with ID : {}", aQuery.getClass().getName(), itemId);
            listGenOrGenAssemb.add(itemId);
        }

        return listGenOrGenAssemb;
    }

    /**
     * Retourne une lise d'item revision de type Unit.
     *
     * @param product the {@link ProductUnit}
     * @return list of Units as {@link ItemRevision}
     * @throws NotFoundException if no object is found
     */
    public List<ItemRevision> getUnitsOfProduct(final ProductUnit product) throws NotFoundException {
        UnitQuery unitQuery = new UnitQuery(session);
        log.trace(SAVED_QUERY + unitQuery.getClass().getName());
        log.trace(SEARCH_UNIT_OF_PRODUCT_ID + product.getProductID());
        return getObjectUnderProduct(product.getProductID(), unitQuery);
    }

    /**
     * Retourne une lise d'item revision de type Generic.
     *
     * @param generic the {@link Generics}
     * @param product the {@link ProductUnit}
     * @return liste d'item revision de type Generic
     * @throws NotFoundException if no object is found
     */
    public List<ItemRevision> getGenericUnderProduct(final Generics generic, final ProductUnit product)
            throws NotFoundException {
        GenericQuery<ItemRevision> genericQuery = new GenericQuery<>(session);
        genericQuery.searchID(generic.getID());
        return getObjectUnderProduct(product.getProductID(), genericQuery);
    }

    /**
     * Retourne une liste d'item revision de type Generic Assemb.
     *
     * @param generic the {@link Generics}
     * @param product the {@link ProductUnit}
     * @return liste d'item revision de type Generic Assemb
     * @throws NotFoundException if no object is found
     */
    public List<ItemRevision> getGenericAssembUnderProduct(final Generics generic, final ProductUnit product) throws NotFoundException {
        GenericAssembQuery<ItemRevision> genericAssembQuery = new GenericAssembQuery<>(session);
        genericAssembQuery.searchID(generic.getID());
        return getObjectUnderProduct(product.getProductID(), genericAssembQuery);
    }

    private List<ItemRevision> getObjectUnderProduct(final String endItem, final AbstractReplicatedQuery<ItemRevision, ?> aQuery)
            throws NotFoundException {
        aQuery.addEndItemID(endItem);
        log.trace("{} is searching under product {}", aQuery.getClass().getName(), endItem);
        List<ItemRevision> result = aQuery.findAll();
        if (result.isEmpty()) {
            String msg = aQuery.getClass().getName() + " did not return any matching Item Revision...";
            log.error(msg);
            throw new NotFoundException(msg);
        }
        return result;
    }

    /**
     * Search active sequence of a current itemRevision by item_id/rev_id. <br>
     * And return the active item revision, the item revision may not be the same as that in parameter ex:<br>
     * INT : part_xx/A.1 <br>
     * OUT : part_xx/A.3
     *
     * @param itemRevision the ItemRev whose active sequence we seek
     * @return activeItemRevision
     */
    public ItemRevision searchActiveSequence(final ItemRevision itemRevision) {
        List<ItemRevision> listItemRevActiveSeq = new ItemRevisionQuery(session)
                .searchIDAndRevision(itemRevision.get_item_id(), itemRevision.get_item_revision_id()).findAll();
        return listItemRevActiveSeq.get(0);
    }
}
