// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI;

/// <summary>
/// Fluent builder for constructing an <see cref="AgentSkillsProvider"/> backed by a composite source.
/// Intended for advanced scenarios where the simple <see cref="AgentSkillsProvider"/> constructors are insufficient.
/// </summary>
/// <remarks>
/// <para>
/// For simple, single-source scenarios, prefer the <see cref="AgentSkillsProvider"/> constructors directly
/// (e.g., passing a skill directory path or a set of skills). Use this builder when you need one or more
/// of the following advanced capabilities:
/// </para>
/// <list type="bullet">
///   <item><description><strong>Mixed skill types</strong> — combine file-based, code-defined (<see cref="AgentInlineSkill"/>),
///   and class-based (<see cref="AgentClassSkill{TSelf}"/>) skills in a single provider.</description></item>
///   <item><description><strong>Multiple file script runners</strong> — use different script runners for different
///   file skill directories via per-source <c>scriptRunner</c> parameters on
///   <see cref="UseFileSkill"/> / <see cref="UseFileSkills(IEnumerable{string}, AgentFileSkillsSourceOptions?, AgentFileSkillScriptRunner?)"/>.</description></item>
///   <item><description><strong>Skill filtering</strong> — include or exclude skills using a predicate
///   via <see cref="UseFilter"/>.</description></item>
/// </list>
/// <para>
/// Example — combining file-based and code-defined skills:
/// </para>
/// <code>
/// var provider = new AgentSkillsProviderBuilder()
///     .UseFileSkills("/path/to/skills")
///     .UseSkills(myInlineSkill1, myInlineSkill2)
///     .UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
///     .Build();
/// </code>
/// </remarks>
public sealed class AgentSkillsProviderBuilder
{
    private readonly List<Func<AgentFileSkillScriptRunner?, ILoggerFactory?, AgentSkillsSource>> _sourceFactories = [];
    private AgentSkillsProviderOptions? _options;
    private ILoggerFactory? _loggerFactory;
    private AgentFileSkillScriptRunner? _scriptRunner;
    private Func<AgentSkill, AgentSkillsSourceContext, bool>? _filter;
    private bool _disableCaching;
    private CachingAgentSkillsSourceOptions? _cachingOptions;

    /// <summary>
    /// Adds a file-based skill source that discovers skills from a filesystem directory.
    /// </summary>
    /// <param name="skillPath">Path to search for skills.</param>
    /// <param name="options">Optional options that control skill discovery behavior.</param>
    /// <param name="scriptRunner">
    /// Optional runner for file-based scripts. When provided, overrides the builder-level runner
    /// set via <see cref="UseFileScriptRunner"/>.
    /// </param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseFileSkill(string skillPath, AgentFileSkillsSourceOptions? options = null, AgentFileSkillScriptRunner? scriptRunner = null)
    {
        return this.UseFileSkills([skillPath], options, scriptRunner);
    }

    /// <summary>
    /// Adds a file-based skill source that discovers skills from multiple filesystem directories.
    /// </summary>
    /// <param name="skillPaths">Paths to search for skills.</param>
    /// <param name="options">Optional options that control skill discovery behavior.</param>
    /// <param name="scriptRunner">
    /// Optional runner for file-based scripts. When provided, overrides the builder-level runner
    /// set via <see cref="UseFileScriptRunner"/>.
    /// </param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseFileSkills(IEnumerable<string> skillPaths, AgentFileSkillsSourceOptions? options = null, AgentFileSkillScriptRunner? scriptRunner = null)
    {
        this._sourceFactories.Add((builderScriptRunner, loggerFactory) =>
        {
            var resolvedRunner = scriptRunner
                ?? builderScriptRunner
                ?? throw new InvalidOperationException($"File-based skill sources require a script runner. Call {nameof(this.UseFileScriptRunner)} or pass a runner to {nameof(this.UseFileSkill)}/{nameof(this.UseFileSkills)}.");
            return new AgentFileSkillsSource(skillPaths, resolvedRunner, options, loggerFactory);
        });
        return this;
    }

    /// <summary>
    /// Adds a single skill.
    /// </summary>
    /// <param name="skill">The skill to add.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseSkill(AgentSkill skill)
    {
        return this.UseSkills(skill);
    }

    /// <summary>
    /// Adds one or more skills.
    /// </summary>
    /// <param name="skills">The skills to add.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseSkills(params AgentSkill[] skills)
    {
        var source = new AgentInMemorySkillsSource(skills);
        this._sourceFactories.Add((_, _) => source);
        return this;
    }

    /// <summary>
    /// Adds skills from the specified collection.
    /// </summary>
    /// <param name="skills">The skills to add.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseSkills(IEnumerable<AgentSkill> skills)
    {
        var source = new AgentInMemorySkillsSource(skills);
        this._sourceFactories.Add((_, _) => source);
        return this;
    }

    /// <summary>
    /// Adds a custom skill source.
    /// </summary>
    /// <remarks>
    /// The provider returned by <see cref="Build"/> takes ownership of <paramref name="source"/> and
    /// disposes it when the provider is disposed. Because the same instance is reused on every
    /// <see cref="Build"/> call, do not build more than one provider from a builder that captures a
    /// shared <paramref name="source"/>; otherwise disposing one provider would dispose the source out
    /// from under the others. To build multiple providers, use the
    /// <see cref="UseSource(Func{ILoggerFactory?, AgentSkillsSource})"/> overload, which creates a fresh
    /// source per build, or pass the source directly to an <see cref="AgentSkillsProvider"/> constructor
    /// with <c>ownsSource: false</c> to retain ownership.
    /// </remarks>
    /// <param name="source">The custom skill source.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseSource(AgentSkillsSource source)
    {
        _ = Throw.IfNull(source);
        this._sourceFactories.Add((_, _) => source);
        return this;
    }

    /// <summary>
    /// Adds a custom skill source created by a factory that receives the builder's logger factory
    /// at build time. Use this overload when the source needs logging and should not require the
    /// caller to pass an <see cref="ILoggerFactory"/> explicitly.
    /// </summary>
    /// <param name="factory">A factory that creates the skill source given an optional logger factory.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseSource(Func<ILoggerFactory?, AgentSkillsSource> factory)
    {
        _ = Throw.IfNull(factory);
        this._sourceFactories.Add((_, loggerFactory) => factory(loggerFactory));
        return this;
    }

    /// <summary>
    /// Sets a custom system prompt template.
    /// </summary>
    /// <param name="promptTemplate">The prompt template with <c>{skills}</c> placeholder for the skills list.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UsePromptTemplate(string promptTemplate)
    {
        this.GetOrCreateOptions().SkillsInstructionPrompt = promptTemplate;
        return this;
    }

    /// <summary>
    /// Sets the runner for file-based skill scripts.
    /// </summary>
    /// <param name="runner">The delegate that runs file-based scripts.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseFileScriptRunner(AgentFileSkillScriptRunner runner)
    {
        this._scriptRunner = Throw.IfNull(runner);
        return this;
    }

    /// <summary>
    /// Sets the logger factory.
    /// </summary>
    /// <param name="loggerFactory">The logger factory.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseLoggerFactory(ILoggerFactory loggerFactory)
    {
        this._loggerFactory = loggerFactory;
        return this;
    }

    /// <summary>
    /// Sets a filter predicate that controls which skills are included.
    /// </summary>
    /// <remarks>
    /// Skills for which the predicate returns <see langword="true"/> are kept;
    /// others are excluded. Only one filter is supported; calling this method
    /// again replaces any previously set filter.
    /// </remarks>
    /// <param name="predicate">A predicate that determines which skills to include.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseFilter(Func<AgentSkill, AgentSkillsSourceContext, bool> predicate)
    {
        _ = Throw.IfNull(predicate);
        this._filter = predicate;
        return this;
    }

    /// <summary>
    /// Configures the <see cref="AgentSkillsProviderOptions"/> using the provided delegate.
    /// </summary>
    /// <param name="configure">A delegate to configure the options.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseOptions(Action<AgentSkillsProviderOptions> configure)
    {
        _ = Throw.IfNull(configure);
        configure(this.GetOrCreateOptions());
        return this;
    }

    /// <summary>
    /// Disables caching of the resolved skill list. By default, skills are fetched once and cached;
    /// calling this method causes the source pipeline to be invoked on every request.
    /// </summary>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder DisableCaching()
    {
        this._disableCaching = true;
        return this;
    }

    /// <summary>
    /// Configures skill caching behavior.
    /// </summary>
    /// <param name="configure">A delegate to configure caching options.</param>
    /// <returns>This builder instance for chaining.</returns>
    public AgentSkillsProviderBuilder UseCachingOptions(Action<CachingAgentSkillsSourceOptions> configure)
    {
        _ = Throw.IfNull(configure);
        this._cachingOptions ??= new CachingAgentSkillsSourceOptions();
        configure(this._cachingOptions);
        return this;
    }

    /// <summary>
    /// Builds the <see cref="AgentSkillsProvider"/>.
    /// </summary>
    /// <remarks>
    /// The returned provider owns the source pipeline constructed by this builder, so disposing the
    /// provider disposes the pipeline (including any sources added to this builder).
    /// <para>
    /// Build more than one provider from the same builder only when every source it produces is
    /// independent per build (for example, sources added via
    /// <see cref="UseSource(Func{ILoggerFactory?, AgentSkillsSource})"/>). A source captured as a shared
    /// instance through <see cref="UseSource(AgentSkillsSource)"/> is reused across builds and would be
    /// disposed by whichever provider is disposed first; build only one provider in that case.
    /// </para>
    /// </remarks>
    /// <returns>A configured <see cref="AgentSkillsProvider"/>.</returns>
    public AgentSkillsProvider Build()
    {
        var resolvedSources = new List<AgentSkillsSource>(this._sourceFactories.Count);
        foreach (var factory in this._sourceFactories)
        {
            resolvedSources.Add(factory(this._scriptRunner, this._loggerFactory));
        }

        AgentSkillsSource source;
        if (resolvedSources.Count == 1)
        {
            source = resolvedSources[0];
        }
        else
        {
            source = new AggregatingAgentSkillsSource(resolvedSources);
        }

        if (!this._disableCaching)
        {
            source = new CachingAgentSkillsSource(source, this._cachingOptions);
        }

        // Apply user-specified filter, then dedup.
        if (this._filter != null)
        {
            source = new FilteringAgentSkillsSource(source, this._filter, this._loggerFactory);
        }

        source = new DeduplicatingAgentSkillsSource(source, this._loggerFactory);

        return new AgentSkillsProvider(source, this._options, this._loggerFactory, ownsSource: true);
    }

    private AgentSkillsProviderOptions GetOrCreateOptions()
    {
        return this._options ??= new AgentSkillsProviderOptions();
    }
}
