/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nifi.processor.util.pattern;

import org.apache.nifi.flowfile.FlowFile;
import org.apache.nifi.logging.ComponentLog;
import org.apache.nifi.processor.ProcessContext;
import org.apache.nifi.processor.ProcessSession;
import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.util.pattern.ErrorTypes.Result;

import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * <p>ExceptionHandler provides a structured Exception handling logic composed by reusable partial functions.
 *
 * <p>
 *     Benefits of using ExceptionHandler:
 *     <li>Externalized error handling code which provides cleaner program only focusing on the expected path.</li>
 *     <li>Classify specific Exceptions into {@link ErrorTypes}, consolidated error handling based on error type.</li>
 *     <li>Context aware error handling, {@link RollbackOnFailure} for instance.</li>
 * </p>
 */
public class ExceptionHandler<C> {

    @FunctionalInterface
    public interface Procedure<I> {
        void apply(I input) throws Exception;
    }

    public interface OnError<C, I> {
        void apply(C context, I input, Result result, Exception e);

        default OnError<C, I> andThen(OnError<C, I> after) {
            return (c, i, r, e) -> {
                apply(c, i, r, e);
                after.apply(c, i, r, e);
            };
        }
    }

    /**
     * Simply categorise an Exception.
     */
    private Function<Exception, ErrorTypes> mapException;

    /**
     * Adjust error type based on the context.
     */
    private BiFunction<C, ErrorTypes, Result> adjustError;

    /**
     * Do some action to the input based on the final error type.
     */
    private OnError<C, ?> onError;

    /**
     * Specify a function that maps an Exception to certain ErrorType.
     */
    public void mapException(Function<Exception, ErrorTypes> mapException) {
        this.mapException = mapException;
    }

    /**
     * <p>Specify a function that adjust ErrorType based on a function context.
     * <p>For example, {@link RollbackOnFailure#createAdjustError(ComponentLog)} decides
     * whether a process session should rollback or transfer input to failure or retry.
     */
    public void adjustError(BiFunction<C, ErrorTypes, Result> adjustError) {
        this.adjustError = adjustError;
    }

    /**
     * <p>Specify a default OnError function that will be called if one is not explicitly specified when {@link #execute(Object, Object, Procedure)} is called.
     */
    public void onError(OnError<C, ?> onError) {
        this.onError = onError;
    }

    /**
     * <p>Executes specified procedure function with the input.
     * <p>Default OnError function will be called when an exception is thrown.
     * @param context function context
     * @param input input for procedure
     * @param procedure a function that does something with the input
     * @return True if the procedure finished without issue. False if procedure threw an Exception but it was handled by {@link OnError}.
     * @throws ProcessException Thrown if the exception was not handled by {@link OnError}
     * @throws DiscontinuedException Indicating the exception was handled by {@link OnError} but process should stop immediately
     * without processing any further input
     */
    @SuppressWarnings("unchecked")
    public <I> boolean execute(C context, I input, Procedure<I> procedure) throws ProcessException, DiscontinuedException {
        return execute(context, input, procedure, (OnError<C, I>) onError);
    }

    /**
     * <p>Executes specified procedure function with the input.
     * @param context function context
     * @param input input for procedure
     * @param procedure a function that does something with the input
     * @param onError specify {@link OnError} function for this execution
     * @return True if the procedure finished without issue. False if procedure threw an Exception but it was handled by {@link OnError}.
     * @throws ProcessException Thrown if the exception was not handled by {@link OnError}
     * @throws DiscontinuedException Indicating the exception was handled by {@link OnError} but process should stop immediately
     * without processing any further input
     */
    public <I> boolean execute(C context, I input, Procedure<I> procedure, OnError<C, I> onError) throws ProcessException, DiscontinuedException {
        try {
            procedure.apply(input);
            return true;
        } catch (Exception e) {

            if (mapException == null) {
                throw new ProcessException("An exception was thrown: " + e, e);
            }

            final ErrorTypes type = mapException.apply(e);

            final Result result;
            if (adjustError != null) {
                result = adjustError.apply(context, type);
            } else {
                result = new Result(type.destination(), type.penalty());
            }

            if (onError == null) {
                throw new IllegalStateException("OnError is not set.");
            }

            onError.apply(context, input, result, e);
        }
        return false;
    }

    private static FlowFile penalize(final ProcessContext context, final ProcessSession session,
                                     final FlowFile flowFile, final ErrorTypes.Penalty penalty) {
        switch (penalty) {
            case Penalize:
                return session.penalize(flowFile);
            case Yield:
                context.yield();
        }
        return flowFile;
    }

    /**
     * Create a {@link OnError} function instance that routes input based on {@link Result} destination and penalty.
     * @param context process context is used to yield a processor
     * @param session process session is used to penalize a FlowFile
     * @param routingResult input FlowFile will be routed to a destination relationship in this {@link RoutingResult}
     * @param relFailure specify failure relationship of a processor
     * @param relRetry specify retry relationship of a processor
     * @return composed function
     */
    public static <C> ExceptionHandler.OnError<C, FlowFile> createOnError(
            final ProcessContext context, final ProcessSession session, final RoutingResult routingResult,
            final Relationship relFailure, final Relationship relRetry) {

        return (fc, input, result, e) -> {
            final PartialFunctions.FlowFileGroup flowFileGroup = () -> Collections.singletonList(input);
            createOnGroupError(context, session, routingResult, relFailure, relRetry).apply(fc, flowFileGroup, result, e);
        };
    }

    /**
     * Same as {@link #createOnError(ProcessContext, ProcessSession, RoutingResult, Relationship, Relationship)} for FlowFileGroup.
     * @param context process context is used to yield a processor
     * @param session process session is used to penalize FlowFiles
     * @param routingResult input FlowFiles will be routed to a destination relationship in this {@link RoutingResult}
     * @param relFailure specify failure relationship of a processor
     * @param relRetry specify retry relationship of a processor
     * @return composed function
     */
    public static <C, I extends PartialFunctions.FlowFileGroup> ExceptionHandler.OnError<C, I> createOnGroupError(
            final ProcessContext context, final ProcessSession session, final RoutingResult routingResult,
            final Relationship relFailure, final Relationship relRetry) {
        return (c, g, r, e) -> {
            final Relationship routeTo;
            switch (r.destination()) {
                case Failure:
                    routeTo = relFailure;
                    break;
                case Retry:
                    routeTo = relRetry;
                    break;
                case Self:
                    routeTo = Relationship.SELF;
                    break;
                default:
                    if (e instanceof ProcessException) {
                        throw (ProcessException) e;
                    } else {
                        Object inputs = null;
                        if (g != null) {
                            final List<FlowFile> flowFiles = g.getFlowFiles();
                            inputs = switch (flowFiles.size()) {
                                case 0 -> "[]";
                                case 1 -> flowFiles.getFirst();
                                default ->
                                        String.format("%d FlowFiles including %s", flowFiles.size(), flowFiles.get(0));
                            };
                        }
                        throw new ProcessException(String.format("Failed to process %s due to %s", inputs, e), e);
                    }
            }
            for (FlowFile f : g.getFlowFiles()) {
                final FlowFile maybePenalized = penalize(context, session, f, r.penalty());
                routingResult.routeTo(maybePenalized, routeTo);
            }
        };
    }
}
