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

// Hosted-AgentSkills
//
// Demonstrates how to host an agent that loads its behavioral guidelines from Foundry Skills at
// startup. Skills are authored as SKILL.md files, uploaded to Foundry via the Skills REST API,
// and downloaded by the agent on boot so guideline updates ship without code changes.
//
// The agent uses AgentSkillsProvider from the Agent Framework which implements the progressive
// disclosure pattern from the Agent Skills specification (https://agentskills.io/):
//   1. Advertise — skill names and descriptions are injected into the system prompt.
//   2. Load — the model calls load_skill to retrieve the full SKILL.md body on demand.
//
// IMPORTANT: In production, skill provisioning (uploading SKILL.md files to Foundry) is an
// external concern — it is NOT the hosted agent's responsibility. The provisioning helper below
// is included for sample convenience only, so the sample is self-contained and runnable without
// a separate setup step. A real deployment pipeline would provision skills separately (e.g., via
// a CI/CD step, a CLI script, or a management portal).

#pragma warning disable AAIP001 // ProjectAgentSkills is experimental

using System.ClientModel;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;

// Load .env file if present (for local development)
Env.TraversePath().Load();

string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
    ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string skillNames = Environment.GetEnvironmentVariable("SKILL_NAMES")
    ?? throw new InvalidOperationException("SKILL_NAMES is not set. Provide a comma-separated list of skill names (e.g., support-style,escalation-policy).");

string[] requestedSkills = skillNames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (requestedSkills.Length == 0)
{
    throw new InvalidOperationException("SKILL_NAMES must list at least one skill name.");
}

// Validate skill names to prevent path traversal.
foreach (string name in requestedSkills)
{
    if (name.Contains('.') || name.Contains('/') || name.Contains('\\') || Path.IsPathRooted(name))
    {
        throw new InvalidOperationException(
            $"Invalid skill name '{name}': skill names must not contain path separators or dots.");
    }
}

// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
    new DevTemporaryTokenCredential(),
    new DefaultAzureCredential());

AIProjectClient projectClient = new(new Uri(endpoint), credential);
ProjectAgentSkills skillsClient = projectClient.AgentAdministrationClient.GetAgentSkills();

// ── Provision skills (sample convenience only — NOT a production pattern) ─────
// In production, skills are provisioned externally (e.g., via CI/CD or a management script).
// This helper ensures the sample's SKILL.md files exist in Foundry so the sample is runnable
// out of the box without a separate setup step. Set PROVISION_SAMPLE_SKILLS=true to enable.
string sourceSkillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
bool provisionEnabled = string.Equals(
    Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase);
if (provisionEnabled && Directory.Exists(sourceSkillsDir))
{
    await EnsureSkillsProvisionedAsync(skillsClient, sourceSkillsDir, requestedSkills);
}

// ── Download skills from Foundry ─────────────────────────────────────────────
// Pull the latest copy of each skill from Foundry into a runtime-only folder.
// This directory is recreated on every startup so the agent always picks up
// the latest version of each skill.
string downloadedSkillsDir = Path.Combine(AppContext.BaseDirectory, "downloaded_skills");
await DownloadSkillsAsync(skillsClient, requestedSkills, downloadedSkillsDir);

// ── Wire skills into the agent ───────────────────────────────────────────────
// AgentSkillsProvider implements progressive disclosure: skill names and descriptions
// are advertised in the system prompt (~100 tokens per skill), and the full SKILL.md
// body is loaded on demand when the model calls the load_skill tool.
AgentSkillsProvider skillsProvider = new(downloadedSkillsDir);

AIAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
{
    Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills",
    ChatOptions = new ChatOptions
    {
        ModelId = deploymentName,
        Instructions = "You are a customer-support assistant for Contoso Outdoors.",
    },
    AIContextProviders = [skillsProvider]
})
.AsBuilder()
.UseToolApproval(new ToolApprovalAgentOptions
{
    AutoApprovalRules = [AgentSkillsProvider.AllToolsAutoApprovalRule],
})
.Build();

// Host the agent as a Foundry Hosted Agent using the Responses API.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);

var app = builder.Build();
app.MapFoundryResponses();

// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();

app.Run();

// ── Helpers ──────────────────────────────────────────────────────────────────

// Downloads each named skill from Foundry into a separate subdirectory under the target directory.
// GetSkillContentAsync downloads the skill package and unzips it into the destination directory.
static async Task DownloadSkillsAsync(ProjectAgentSkills skillsClient, string[] skillNames, string targetDir)
{
    if (Directory.Exists(targetDir))
    {
        Directory.Delete(targetDir, recursive: true);
    }

    Directory.CreateDirectory(targetDir);

    foreach (string name in skillNames)
    {
        Console.WriteLine($"Downloading skill '{name}' from Foundry...");

        string skillDir = Path.Combine(targetDir, name);
        Directory.CreateDirectory(skillDir);

        await skillsClient.GetSkillContentAsync(name, skillDir);

        if (!File.Exists(Path.Combine(skillDir, "SKILL.md")))
        {
            throw new InvalidOperationException(
                $"Downloaded skill '{name}' did not contain a SKILL.md at the root.");
        }
    }
}

// Ensures each requested skill is provisioned in Foundry. For each skill name, checks whether
// the skill exists and uploads it from the local source directory if it does not.
//
// This is a sample convenience helper — in production, skill provisioning is an external concern.
static async Task EnsureSkillsProvisionedAsync(ProjectAgentSkills skillsClient, string sourceDir, string[] skillNames)
{
    foreach (string name in skillNames)
    {
        string skillPath = Path.Combine(sourceDir, name);
        if (!Directory.Exists(skillPath) || !File.Exists(Path.Combine(skillPath, "SKILL.md")))
        {
            continue; // No local source for this skill — skip provisioning.
        }

        try
        {
            await skillsClient.GetSkillAsync(name);
            Console.WriteLine($"Skill '{name}' already exists in Foundry.");
        }
        catch (ClientResultException ex) when (ex.Status == 404)
        {
            Console.WriteLine($"Provisioning skill '{name}' from {skillPath}...");
            AgentsSkill imported = (await skillsClient.CreateSkillVersionFromFilesAsync(name, skillPath)).Value;
            Console.WriteLine($"  Imported skill '{imported.Name}' (id={imported.Id}, version={imported.LatestVersion}).");
        }
    }
}
