/*
 * SPDX-License-Identifier: MIT
 */
package org.ta4j.core.serialization;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Locale;

import org.ta4j.core.BarSeries;
import org.ta4j.core.BaseStrategy;
import org.ta4j.core.Rule;
import org.ta4j.core.Strategy;
import org.ta4j.core.Trade.TradeType;
import org.ta4j.core.strategy.named.NamedStrategy;

/**
 * Serializes and deserializes {@link Strategy} instances into structured
 * {@link ComponentDescriptor} payloads.
 *
 * @since 0.19
 */
public final class StrategySerialization {

    private static final String ENTRY_LABEL = "entry";
    private static final String EXIT_LABEL = "exit";
    private static final String STRATEGY_PACKAGE = "org.ta4j.core";
    private static final String UNSTABLE_BARS_KEY = "unstableBars";
    private static final String STARTING_TYPE_KEY = "startingType";
    private static final String ARGS_KEY = "__args";

    private StrategySerialization() {
    }

    /**
     * Serializes a {@link Strategy} to a JSON payload.
     *
     * @param strategy strategy instance
     * @return JSON representation
     */
    public static String toJson(Strategy strategy) {
        return ComponentSerialization.toJson(describe(strategy));
    }

    /**
     * Converts a {@link Strategy} into a {@link ComponentDescriptor} hierarchy.
     *
     * @param strategy strategy instance
     * @return descriptor representing the strategy
     */
    public static ComponentDescriptor describe(Strategy strategy) {
        Objects.requireNonNull(strategy, "strategy");

        if (strategy instanceof NamedStrategy) {
            return strategy.toDescriptor();
        }

        ComponentDescriptor entryDescriptor = RuleSerialization.describe(strategy.getEntryRule());
        ComponentDescriptor exitDescriptor = RuleSerialization.describe(strategy.getExitRule());

        Class<?> strategyClass = strategy.getClass();
        String typeName = strategyClass.getPackageName().equals(STRATEGY_PACKAGE) ? strategyClass.getSimpleName()
                : strategyClass.getName();
        ComponentDescriptor.Builder builder = ComponentDescriptor.builder().withType(typeName);

        String name = strategy.getName();
        if (name != null && !name.isBlank()) {
            builder.withLabel(name);
        }

        Map<String, Object> parameters = new LinkedHashMap<>();
        parameters.put(UNSTABLE_BARS_KEY, strategy.getUnstableBars());
        if (strategy.getStartingType() != TradeType.BUY) {
            parameters.put(STARTING_TYPE_KEY, strategy.getStartingType().name());
        }
        builder.withParameters(parameters);

        if (entryDescriptor != null) {
            builder.addComponent(applyLabel(entryDescriptor, ENTRY_LABEL));
        }
        if (exitDescriptor != null) {
            builder.addComponent(applyLabel(exitDescriptor, EXIT_LABEL));
        }

        return builder.build();
    }

    /**
     * Rebuilds a strategy from a JSON payload.
     *
     * @param series bar series to attach to the strategy
     * @param json   JSON representation generated by {@link #toJson(Strategy)}
     * @return reconstructed strategy
     */
    public static Strategy fromJson(BarSeries series, String json) {
        ComponentDescriptor descriptor = ComponentSerialization.parse(json);
        return fromDescriptor(series, descriptor);
    }

    /**
     * Rebuilds a strategy from a descriptor tree.
     * <p>
     * If the specified strategy type cannot be instantiated (e.g., no matching
     * constructor is found), this method will silently fall back to creating a
     * {@link BaseStrategy} instance with the same entry/exit rules and parameters.
     * This fallback behavior may mask configuration issues where a specific
     * strategy type was expected but could not be constructed. Callers should
     * verify the returned strategy type matches expectations if strict type
     * checking is required.
     *
     * @param series     bar series to attach to the strategy
     * @param descriptor descriptor describing the strategy
     * @return reconstructed strategy (may be a {@link BaseStrategy} fallback if the
     *         specified type could not be instantiated)
     */
    public static Strategy fromDescriptor(BarSeries series, ComponentDescriptor descriptor) {
        Objects.requireNonNull(series, "series");
        Objects.requireNonNull(descriptor, "descriptor");

        String descriptorType = descriptor.getType();
        if (NamedStrategy.SERIALIZED_TYPE.equals(descriptorType)
                || NamedStrategy.class.getName().equals(descriptorType)) {
            return instantiateNamedStrategy(series, descriptor, null);
        }

        Class<? extends Strategy> strategyType = resolveStrategyClass(descriptorType);
        if (NamedStrategy.class.isAssignableFrom(strategyType)) {
            return instantiateNamedStrategy(series, descriptor, strategyType);
        }

        // Create a Strategy-level context that contains all Strategy components
        // This allows rule deserialization to resolve Strategy-level indicators and
        // rules
        // For now, pass null as parent context - rule components contain all their
        // dependencies
        // and don't need Strategy-level resolution during constructor matching
        Rule entryRule = instantiateRule(series, extractChild(descriptor, ENTRY_LABEL), null);
        Rule exitRule = instantiateRule(series, extractChild(descriptor, EXIT_LABEL), null);

        String name = descriptor.getLabel();
        int unstableBars = extractUnstableBars(descriptor.getParameters().get(UNSTABLE_BARS_KEY));
        TradeType startingType = extractStartingType(descriptor.getParameters().get(STARTING_TYPE_KEY));

        Strategy strategy = instantiateStrategy(strategyType, name, entryRule, exitRule, unstableBars, startingType);
        strategy.setUnstableBars(unstableBars);
        return strategy;
    }

    private static ComponentDescriptor applyLabel(ComponentDescriptor descriptor, String label) {
        if (descriptor == null) {
            return null;
        }
        ComponentDescriptor.Builder builder = ComponentDescriptor.builder()
                .withType(descriptor.getType())
                .withLabel(label);
        if (!descriptor.getParameters().isEmpty()) {
            builder.withParameters(descriptor.getParameters());
        }
        for (ComponentDescriptor component : descriptor.getComponents()) {
            builder.addComponent(component);
        }
        return builder.build();
    }

    private static ComponentDescriptor extractChild(ComponentDescriptor descriptor, String label) {
        for (ComponentDescriptor component : descriptor.getComponents()) {
            if (label.equals(component.getLabel())) {
                return cloneWithoutLabel(component);
            }
        }
        throw new IllegalArgumentException("Missing strategy " + label + " rule descriptor");
    }

    private static ComponentDescriptor cloneWithoutLabel(ComponentDescriptor descriptor) {
        if (descriptor == null) {
            return null;
        }
        ComponentDescriptor.Builder builder = ComponentDescriptor.builder().withType(descriptor.getType());
        if (!descriptor.getParameters().isEmpty()) {
            builder.withParameters(descriptor.getParameters());
        }
        for (ComponentDescriptor component : descriptor.getComponents()) {
            builder.addComponent(component);
        }
        return builder.build();
    }

    private static int extractUnstableBars(Object value) {
        if (value == null) {
            return 0;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException e) {
            return 0;
        }
    }

    private static TradeType extractStartingType(Object value) {
        if (value == null) {
            return TradeType.BUY;
        }
        if (value instanceof TradeType tradeType) {
            return tradeType;
        }
        try {
            return TradeType.valueOf(String.valueOf(value).trim().toUpperCase(Locale.ROOT));
        } catch (IllegalArgumentException ex) {
            return TradeType.BUY;
        }
    }

    private static Rule instantiateRule(BarSeries series, ComponentDescriptor descriptor) {
        return instantiateRule(series, descriptor, null);
    }

    private static Rule instantiateRule(BarSeries series, ComponentDescriptor descriptor,
            RuleSerialization.ReconstructionContext parentContext) {
        if (descriptor == null) {
            throw new IllegalArgumentException("Rule descriptor cannot be null");
        }

        // Check if this is a rule by type name (contains "Rule")
        String type = descriptor.getType();
        if (type != null && type.contains("Rule")) {
            return RuleSerialization.fromDescriptor(series, descriptor, parentContext);
        }

        // Legacy check for __args (for backwards compatibility)
        if (descriptor.getParameters().containsKey(ARGS_KEY)) {
            return RuleSerialization.fromDescriptor(series, descriptor, parentContext);
        }

        if (type == null || type.isBlank()) {
            throw new IllegalArgumentException("Rule descriptor missing type: " + descriptor);
        }
        Class<?> clazz = resolveRuleClass(type);
        if (!Rule.class.isAssignableFrom(clazz)) {
            throw new IllegalArgumentException("Descriptor type does not implement Rule: " + type);
        }
        @SuppressWarnings("unchecked")
        Class<? extends Rule> ruleType = (Class<? extends Rule>) clazz;

        Optional<Rule> instance = invokeFactory(ruleType, series, descriptor);
        if (instance.isPresent()) {
            return instance.get();
        }

        if (!descriptor.getComponents().isEmpty()) {
            List<Rule> components = new ArrayList<>(descriptor.getComponents().size());
            for (ComponentDescriptor component : descriptor.getComponents()) {
                components.add(instantiateRule(series, component, parentContext));
            }
            Optional<Rule> composite = tryCompositeConstructor(ruleType, components);
            if (composite.isPresent()) {
                return composite.get();
            }
        }

        Optional<Rule> constructed = tryDescriptorConstructor(ruleType, series, descriptor);
        if (constructed.isPresent()) {
            return constructed.get();
        }

        throw new IllegalArgumentException("Unable to instantiate rule type: " + type);
    }

    private static Optional<Rule> invokeFactory(Class<? extends Rule> ruleType, BarSeries series,
            ComponentDescriptor descriptor) {
        try {
            Method factory = ruleType.getDeclaredMethod("fromDescriptor", BarSeries.class, ComponentDescriptor.class);
            factory.setAccessible(true);
            return Optional.of((Rule) factory.invoke(null, series, descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore and try the next option
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to invoke factory on rule type: " + ruleType.getName(), ex);
        }

        try {
            Method factory = ruleType.getDeclaredMethod("fromDescriptor", ComponentDescriptor.class);
            factory.setAccessible(true);
            return Optional.of((Rule) factory.invoke(null, descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore and try next
        } catch (IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to invoke factory on rule type: " + ruleType.getName(), ex);
        }

        return Optional.empty();
    }

    private static Optional<Rule> tryCompositeConstructor(Class<? extends Rule> ruleType, List<Rule> components) {
        Constructor<?>[] constructors = ruleType.getDeclaredConstructors();
        for (Constructor<?> constructor : constructors) {
            Class<?>[] parameterTypes = constructor.getParameterTypes();
            if (parameterTypes.length != components.size()) {
                continue;
            }
            boolean matches = true;
            for (Class<?> parameterType : parameterTypes) {
                if (!Rule.class.isAssignableFrom(parameterType)) {
                    matches = false;
                    break;
                }
            }
            if (!matches) {
                continue;
            }
            try {
                constructor.setAccessible(true);
                return Optional.of((Rule) constructor.newInstance(components.toArray()));
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
                throw new IllegalStateException("Failed to construct composite rule: " + ruleType.getName(), ex);
            }
        }
        return Optional.empty();
    }

    private static Optional<Rule> tryDescriptorConstructor(Class<? extends Rule> ruleType, BarSeries series,
            ComponentDescriptor descriptor) {
        try {
            Constructor<? extends Rule> constructor = ruleType.getDeclaredConstructor(BarSeries.class,
                    ComponentDescriptor.class);
            constructor.setAccessible(true);
            return Optional.of(constructor.newInstance(series, descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct rule: " + ruleType.getName(), ex);
        }

        try {
            Constructor<? extends Rule> constructor = ruleType.getDeclaredConstructor(ComponentDescriptor.class);
            constructor.setAccessible(true);
            return Optional.of(constructor.newInstance(descriptor));
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct rule: " + ruleType.getName(), ex);
        }

        try {
            Constructor<? extends Rule> constructor = ruleType.getDeclaredConstructor();
            constructor.setAccessible(true);
            return Optional.of(constructor.newInstance());
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct rule: " + ruleType.getName(), ex);
        }

        return Optional.empty();
    }

    @SuppressWarnings("unchecked")
    private static Class<? extends Rule> resolveRuleClass(String type) {
        try {
            return (Class<? extends Rule>) Class.forName(type);
        } catch (ClassNotFoundException ex) {
            // try ta4j core rules package
        }
        try {
            return (Class<? extends Rule>) Class.forName("org.ta4j.core.rules." + type);
        } catch (ClassNotFoundException ex) {
            throw new IllegalArgumentException("Unknown rule type: " + type, ex);
        }
    }

    @SuppressWarnings("unchecked")
    private static Class<? extends Strategy> resolveStrategyClass(String type) {
        if (type == null || type.isBlank()) {
            return BaseStrategy.class;
        }
        try {
            Class<?> clazz = Class.forName(type);
            if (Strategy.class.isAssignableFrom(clazz)) {
                return (Class<? extends Strategy>) clazz;
            }
        } catch (ClassNotFoundException ex) {
            // ignore and try package-local lookup
        }
        try {
            Class<?> clazz = Class.forName(STRATEGY_PACKAGE + '.' + type);
            if (Strategy.class.isAssignableFrom(clazz)) {
                return (Class<? extends Strategy>) clazz;
            }
        } catch (ClassNotFoundException ex) {
            // ignore and fall back to BaseStrategy
        }
        return BaseStrategy.class;
    }

    /**
     * Attempts to instantiate a strategy of the specified type using various
     * constructor patterns.
     * <p>
     * This method tries multiple constructor signatures in order:
     * <ol>
     * <li>{@code (String, Rule, Rule, int, TradeType)} - name, entry, exit,
     * unstableBars, startingType</li>
     * <li>{@code (String, Rule, Rule, TradeType)} - name, entry, exit, startingType
     * (unstableBars set via setter)</li>
     * <li>{@code (Rule, Rule, int, TradeType)} - entry, exit, unstableBars,
     * startingType</li>
     * <li>{@code (Rule, Rule, TradeType)} - entry, exit, startingType (unstableBars
     * set via setter)</li>
     * <li>{@code (String, Rule, Rule, int)} - name, entry, exit, unstableBars</li>
     * <li>{@code (String, Rule, Rule)} - name, entry, exit (unstableBars set via
     * setter)</li>
     * <li>{@code (Rule, Rule, int)} - entry, exit, unstableBars</li>
     * <li>{@code (Rule, Rule)} - entry, exit (unstableBars set via setter)</li>
     * </ol>
     * <p>
     * <strong>Fallback Behavior:</strong> If none of the above constructors are
     * found and the requested type is not {@link BaseStrategy}, this method will
     * silently create a {@link BaseStrategy} instance instead. This fallback
     * provides resilience but may mask configuration issues where a specific
     * strategy type was expected. The returned instance will have the same entry
     * rule, exit rule, name, and unstableBars value, but will be of type
     * {@code BaseStrategy} rather than the requested type.
     * <p>
     * If the requested type is already {@code BaseStrategy} and no suitable
     * constructor is found, an {@link IllegalStateException} is thrown.
     *
     * @param strategyType the class of strategy to instantiate
     * @param name         strategy name (may be null)
     * @param entryRule    entry rule (required)
     * @param exitRule     exit rule (required)
     * @param unstableBars number of unstable bars
     * @param startingType strategy entry trade type
     * @return instantiated strategy (may be a {@link BaseStrategy} fallback if the
     *         requested type could not be instantiated)
     * @throws IllegalStateException if the requested type is {@code BaseStrategy}
     *                               and no suitable constructor is found
     */
    private static Strategy instantiateStrategy(Class<? extends Strategy> strategyType, String name, Rule entryRule,
            Rule exitRule, int unstableBars, TradeType startingType) {
        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(String.class, Rule.class,
                    Rule.class, int.class, TradeType.class);
            constructor.setAccessible(true);
            return constructor.newInstance(name, entryRule, exitRule, unstableBars, startingType);
        } catch (NoSuchMethodException ex) {
            // ignore and try the next options
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(String.class, Rule.class,
                    Rule.class, TradeType.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(name, entryRule, exitRule, startingType);
            strategy.setUnstableBars(unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore and try the next options
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(Rule.class, Rule.class,
                    int.class, TradeType.class);
            constructor.setAccessible(true);
            return constructor.newInstance(entryRule, exitRule, unstableBars, startingType);
        } catch (NoSuchMethodException ex) {
            // ignore and try the next options
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(Rule.class, Rule.class,
                    TradeType.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(entryRule, exitRule, startingType);
            strategy.setUnstableBars(unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore and try the next options
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(String.class, Rule.class,
                    Rule.class, int.class);
            constructor.setAccessible(true);
            return constructor.newInstance(name, entryRule, exitRule, unstableBars);
        } catch (NoSuchMethodException ex) {
            // ignore and try the next options
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(String.class, Rule.class,
                    Rule.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(name, entryRule, exitRule);
            strategy.setUnstableBars(unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(Rule.class, Rule.class,
                    int.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(entryRule, exitRule, unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(Rule.class, Rule.class);
            constructor.setAccessible(true);
            Strategy strategy = constructor.newInstance(entryRule, exitRule);
            strategy.setUnstableBars(unstableBars);
            return strategy;
        } catch (NoSuchMethodException ex) {
            // ignore
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
            throw new IllegalStateException("Failed to construct strategy: " + strategyType.getName(), ex);
        }

        // Fallback: If the requested strategy type cannot be instantiated, create a
        // BaseStrategy instead. This provides resilience but may mask configuration
        // issues where a specific strategy type was expected. The fallback preserves
        // the entry/exit rules and parameters but changes the strategy type.
        if (!strategyType.equals(BaseStrategy.class)) {
            return new BaseStrategy(name, entryRule, exitRule, unstableBars, startingType);
        }
        throw new IllegalStateException("No suitable constructor found for strategy type: " + strategyType.getName());
    }

    private static Strategy instantiateNamedStrategy(BarSeries series, ComponentDescriptor descriptor,
            Class<? extends Strategy> resolvedType) {
        String label = descriptor.getLabel();
        if (label == null || label.isBlank()) {
            throw new IllegalArgumentException(
                    "Named strategy descriptor missing label (type=" + descriptor.getType() + ")");
        }

        List<String> labelTokens = NamedStrategy.splitLabel(label);
        if (labelTokens.isEmpty()) {
            throw new IllegalArgumentException(
                    "Named strategy label missing strategy identifier: label='" + label + "'");
        }

        String simpleName = labelTokens.get(0);
        if (simpleName == null || simpleName.isBlank()) {
            throw new IllegalArgumentException(
                    "Named strategy label missing strategy identifier (leading underscore or empty token): label='"
                            + label + "'");
        }

        String[] parameters = labelTokens.size() == 1 ? new String[0]
                : labelTokens.subList(1, labelTokens.size()).toArray(new String[0]);

        Class<? extends NamedStrategy> strategyType = resolveNamedStrategyType(simpleName, resolvedType);
        Constructor<? extends Strategy> constructor = findNamedStrategyConstructor(strategyType);
        try {
            return constructor.newInstance(new Object[] { series, parameters });
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new IllegalStateException(
                    "Failed to construct named strategy: " + strategyType.getName() + " (label='" + label + "')", ex);
        } catch (InvocationTargetException ex) {
            Throwable cause = ex.getCause();
            if (cause instanceof IllegalArgumentException) {
                throw new IllegalArgumentException("Named strategy reconstruction failed for label='" + label
                        + "' params=" + Arrays.toString(parameters) + ": " + cause.getMessage(), cause);
            }
            throw new IllegalStateException(
                    "Failed to construct named strategy: " + strategyType.getName() + " (label='" + label + "')",
                    cause);
        }
    }

    @SuppressWarnings("unchecked")
    private static Class<? extends NamedStrategy> resolveNamedStrategyType(String simpleName,
            Class<? extends Strategy> resolvedType) {
        if (resolvedType != null && resolvedType != NamedStrategy.class
                && NamedStrategy.class.isAssignableFrom(resolvedType)) {
            return (Class<? extends NamedStrategy>) resolvedType;
        }
        return NamedStrategy.requireRegistered(simpleName);
    }

    private static Constructor<? extends Strategy> findNamedStrategyConstructor(
            Class<? extends NamedStrategy> strategyType) {
        try {
            Constructor<? extends Strategy> constructor = strategyType.getDeclaredConstructor(BarSeries.class,
                    String[].class);
            constructor.setAccessible(true);
            return constructor;
        } catch (NoSuchMethodException ex) {
            throw new IllegalStateException(
                    "Named strategy missing (BarSeries, String...) constructor: " + strategyType.getName(), ex);
        }
    }

}
