pipeline {
  agent {
    kubernetes {
      yaml """
apiVersion: v1
kind: Pod
spec:
  restartPolicy: Never
  containers:
    - name: dotnet
      image: mcr.microsoft.com/dotnet/sdk:9.0
      command:
        - cat
      tty: true
"""
    }
  }
  options {
    timestamps()
  }
  parameters {
    string(name: 'HARBOR_REPO', defaultValue: 'registry.bstein.dev/streaming/oidc-plugin', description: 'OCI repository for the plugin artifact')
    string(name: 'JELLYFIN_VERSION', defaultValue: '10.11.5', description: 'Jellyfin version to tag the plugin with')
    string(name: 'PLUGIN_VERSION', defaultValue: '1.0.2.0', description: 'Plugin version')
  }
  environment {
    ORAS_VERSION = "1.2.0"
    DOTNET_CLI_TELEMETRY_OPTOUT = "1"
    DOTNET_SKIP_FIRST_TIME_EXPERIENCE = "1"
  }
  stages {
    stage('Checkout') {
      steps {
        container('dotnet') {
          checkout scm
        }
      }
    }
    stage('Build plugin') {
      steps {
        container('dotnet') {
          sh '''
            set -euo pipefail
            apt-get update
            apt-get install -y --no-install-recommends zip curl ca-certificates git
            WORKDIR="$(pwd)/build"
            SRC_DIR="${WORKDIR}/src"
            DIST_DIR="${WORKDIR}/dist"
            ART_DIR="${WORKDIR}/artifact"
            rm -rf "${SRC_DIR}" "${DIST_DIR}" "${ART_DIR}"
            mkdir -p "${SRC_DIR}" "${DIST_DIR}" "${ART_DIR}"
            git clone https://github.com/lolerskatez/JellyfinOIDCPlugin.git "${SRC_DIR}"
            cd "${SRC_DIR}"
            # Override controllers to avoid DI version issues and add injection script
            cat > Controllers/OidcController.cs <<'EOF'
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using IdentityModel.OidcClient;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace JellyfinOIDCPlugin.Controllers;

#nullable enable

[ApiController]
[Route("api/oidc")]
public class OidcController : ControllerBase
{
    private IUserManager UserManager => HttpContext.RequestServices.GetRequiredService<IUserManager>();
    private static readonly Dictionary<string, object> StateManager = new(); // Store AuthorizeState objects

    [HttpGet("start")]
    public async Task<IActionResult> Start()
    {
        var config = Plugin.Instance?.Configuration;
        if (config == null)
        {
            return BadRequest("Plugin not initialized");
        }

        var options = new OidcClientOptions
        {
            Authority = config.OidEndpoint?.Trim(),
            ClientId = config.OidClientId?.Trim(),
            ClientSecret = config.OidSecret?.Trim(),
            RedirectUri = GetRedirectUri(),
            Scope = string.Join(" ", config.OidScopes)
        };

        try
        {
            var client = new OidcClient(options);
            var result = await client.PrepareLoginAsync().ConfigureAwait(false);

            // Store the authorize state for the callback
            var stateString = (string)result.GetType().GetProperty("State")?.GetValue(result);
            if (!string.IsNullOrEmpty(stateString))
            {
                StateManager[stateString] = result;
            }

            var startUrl = (string)result.GetType().GetProperty("StartUrl")?.GetValue(result);
            if (string.IsNullOrEmpty(startUrl))
            {
                Console.WriteLine("OIDC: Could not get StartUrl from OIDC result");
                return BadRequest("OIDC initialization failed");
            }

            return Redirect(startUrl);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC start error: {ex}");
            return BadRequest("OIDC error: " + ex.Message);
        }
    }

    [HttpGet("callback")]
    public async Task<IActionResult> Callback()
    {
        var config = Plugin.Instance?.Configuration;
        if (config == null)
        {
            return BadRequest("Plugin not initialized");
        }

        try
        {
            var stateParam = Request.Query["state"].ToString();
            if (string.IsNullOrEmpty(stateParam) || !StateManager.TryGetValue(stateParam, out var storedState))
            {
                Console.WriteLine($"OIDC: Invalid state {stateParam}");
                return BadRequest("Invalid state");
            }

            var options = new OidcClientOptions
            {
                Authority = config.OidEndpoint?.Trim(),
                ClientId = config.OidClientId?.Trim(),
                ClientSecret = config.OidSecret?.Trim(),
                RedirectUri = GetRedirectUri(),
                Scope = string.Join(" ", config.OidScopes)
            };

            var client = new OidcClient(options);
            // Cast stored state to AuthorizeState - it's stored as object
            var authorizeState = (AuthorizeState)storedState;
            var result = await client.ProcessResponseAsync(Request.QueryString.Value, authorizeState).ConfigureAwait(false);

            if (result.IsError)
            {
                Console.WriteLine($"OIDC callback failed: {result.Error} - {result.ErrorDescription}");
                return BadRequest("OIDC authentication failed");
            }

            // Get email from claims
            var email = result.User?.FindFirst("email")?.Value ?? 
                       result.User?.FindFirst("preferred_username")?.Value ??
                       result.User?.FindFirst("sub")?.Value;
            
            if (string.IsNullOrEmpty(email))
            {
                Console.WriteLine("OIDC: No email/username found in OIDC response");
                return BadRequest("No email/username found in OIDC response");
            }

            // Get or create user
            var user = UserManager.GetUserByName(email);
            if (user == null)
            {
                Console.WriteLine($"OIDC: Creating new user {email}");
                user = await UserManager.CreateUserAsync(email).ConfigureAwait(false);
            }

            // Set authentication provider
            user.AuthenticationProviderId = "OIDC";

            // Get roles from claims
            var rolesClaimValue = result.User?.FindFirst(config.RoleClaim)?.Value;
            var roles = string.IsNullOrEmpty(rolesClaimValue)
                ? Array.Empty<string>()
                : rolesClaimValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);

            // Set permissions based on groups
            var isAdmin = roles.Any(r => r.Equals("admin", StringComparison.OrdinalIgnoreCase));
            var isPowerUser = roles.Any(r => r.Equals("Power User", StringComparison.OrdinalIgnoreCase)) && !isAdmin;

            Console.WriteLine($"OIDC: User {email} authenticated. Admin: {isAdmin}, PowerUser: {isPowerUser}");

            // Update user in database
            await UserManager.UpdateUserAsync(user).ConfigureAwait(false);

            StateManager.Remove(stateParam);

            // Redirect to Jellyfin main page
            return Redirect("/");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC callback error: {ex}");
            return BadRequest("OIDC error: " + ex.Message);
        }
    }

    [HttpPost("token")]
    public async Task<IActionResult> ExchangeToken([FromBody] TokenExchangeRequest request)
    {
        var config = Plugin.Instance?.Configuration;
        if (config == null)
        {
            Console.WriteLine("OIDC: Plugin not initialized");
            return BadRequest("Plugin not initialized");
        }

        if (string.IsNullOrEmpty(request?.AccessToken))
        {
            Console.WriteLine("OIDC: No access token provided");
            return BadRequest("Access token is required");
        }

        try
        {
            Console.WriteLine("OIDC: Processing token exchange request");

            // Validate the token with the OIDC provider using UserInfo endpoint
            var options = new OidcClientOptions
            {
                Authority = config.OidEndpoint?.Trim(),
                ClientId = config.OidClientId?.Trim(),
                ClientSecret = config.OidSecret?.Trim(),
                Scope = string.Join(" ", config.OidScopes)
            };

            var client = new OidcClient(options);
            
            // Use the access token to get user info
            var userInfoResult = await client.GetUserInfoAsync(request.AccessToken).ConfigureAwait(false);
            
            if (userInfoResult.IsError)
            {
                Console.WriteLine($"OIDC: Failed to get user info: {userInfoResult.Error}");
                return Unauthorized("Invalid access token");
            }

            // Extract email/username from user info
            var email = userInfoResult.Claims.FirstOrDefault(c => c.Type == "email")?.Value ??
                       userInfoResult.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value ??
                       userInfoResult.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;

            if (string.IsNullOrEmpty(email))
            {
                Console.WriteLine("OIDC: No email/username found in token");
                return BadRequest("No email/username found in token");
            }

            // Get or create user
            var user = UserManager.GetUserByName(email);
            if (user == null)
            {
                if (!config.AutoCreateUser)
                {
                    Console.WriteLine($"OIDC: User {email} not found and auto-create disabled");
                    return Unauthorized("User does not exist and auto-creation is disabled");
                }

                Console.WriteLine($"OIDC: Creating new user from token {email}");
                user = await UserManager.CreateUserAsync(email).ConfigureAwait(false);
            }

            // Update user authentication provider
            user.AuthenticationProviderId = "OIDC";

            // Get roles from claims
            var rolesClaimName = config.RoleClaim ?? "groups";
            var rolesClaimValue = userInfoResult.Claims.FirstOrDefault(c => c.Type == rolesClaimName)?.Value;
            var roles = string.IsNullOrEmpty(rolesClaimValue)
                ? Array.Empty<string>()
                : rolesClaimValue.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);

            // Set permissions based on groups
            var isAdmin = roles.Any(r => r.Equals("admin", StringComparison.OrdinalIgnoreCase));
            var isPowerUser = roles.Any(r => r.Equals("Power User", StringComparison.OrdinalIgnoreCase)) && !isAdmin;

            Console.WriteLine($"OIDC: Token exchange for {email} Admin:{isAdmin} Power:{isPowerUser}");

            // Update user in database
            await UserManager.UpdateUserAsync(user).ConfigureAwait(false);

            // Return success with user info
            return Ok(new TokenExchangeResponse
            {
                Success = true,
                UserId = user.Id.ToString(),
                Username = user.Username,
                Email = email,
                IsAdmin = isAdmin,
                Message = "User authenticated successfully"
            });
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC token exchange error: {ex}");
            return StatusCode(500, $"Token exchange failed: {ex.Message}");
        }
    }

    private string GetRedirectUri()
    {
        var configured = Plugin.Instance?.Configuration?.RedirectUri;
        if (!string.IsNullOrWhiteSpace(configured))
        {
            return configured!;
        }

        return $"{Request.Scheme}://{Request.Host}/api/oidc/callback";
    }
}

public class TokenExchangeRequest
{
    public string? AccessToken { get; set; }
    public string? IdToken { get; set; }
}

public class TokenExchangeResponse
{
    public bool Success { get; set; }
    public string? UserId { get; set; }
    public string? Username { get; set; }
    public string? Email { get; set; }
    public bool IsAdmin { get; set; }
    public string? Message { get; set; }
}
EOF

            cat > Controllers/OidcStaticController.cs <<'EOF'
using System;
using System.IO;
using System.Reflection;
using MediaBrowser.Common.Plugins;
using Microsoft.AspNetCore.Mvc;

namespace JellyfinOIDCPlugin.Controllers;

[ApiController]
[Route("api/oidc")]
public class OidcStaticController : ControllerBase
{
    [HttpGet("login.js")]
    public IActionResult GetLoginScript()
    {
        try
        {
            var assembly = Assembly.GetExecutingAssembly();
            using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-login.js");
            if (stream == null)
            {
                Console.WriteLine("OIDC: Login script resource not found");
                return NotFound();
            }

            using var reader = new StreamReader(stream);
            var content = reader.ReadToEnd();
            
            return Content(content, "application/javascript");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC: Error serving login script {ex}");
            return StatusCode(500, "Error loading login script");
        }
    }

    [HttpGet("loader.js")]
    public IActionResult GetLoader()
    {
        try
        {
            var assembly = Assembly.GetExecutingAssembly();
            using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-loader.js");
            if (stream == null)
            {
                Console.WriteLine("OIDC: Loader script resource not found");
                return NotFound();
            }

            using var reader = new StreamReader(stream);
            var content = reader.ReadToEnd();
            
            return Content(content, "application/javascript");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC: Error serving loader script {ex}");
            return StatusCode(500, "Error loading loader script");
        }
    }

    [HttpGet("inject")]
    public IActionResult GetInject()
    {
        try
        {
            var script = @"
(function() {
    console.log('[OIDC Plugin] Bootstrap inject started');
    
    // Load oidc-loader.js dynamically
    const loaderScript = document.createElement('script');
    loaderScript.src = '/api/oidc/loader.js';
    loaderScript.type = 'application/javascript';
    loaderScript.onerror = function() {
        console.error('[OIDC Plugin] Failed to load loader.js');
    };
    loaderScript.onload = function() {
        console.log('[OIDC Plugin] Loader.js loaded successfully');
    };
    
    // Append to head or body
    const target = document.head || document.documentElement;
    target.appendChild(loaderScript);
    
    console.log('[OIDC Plugin] Bootstrap script appended to page');
})();
";
            return Content(script, "application/javascript");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC: Error serving inject script {ex}");
            return StatusCode(500, "Error loading inject script");
        }
    }

    [HttpGet("global.js")]
    public IActionResult GetGlobalInjector()
    {
        try
        {
            var assembly = Assembly.GetExecutingAssembly();
            using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.oidc-global-injector.js");
            if (stream == null)
            {
                Console.WriteLine("OIDC: Global injector resource not found");
                return NotFound();
            }

            using var reader = new StreamReader(stream);
            var content = reader.ReadToEnd();
            
            return Content(content, "application/javascript");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC: Error serving global injector {ex}");
            return StatusCode(500, "Error loading global injector");
        }
    }

    [HttpGet("config")]
    public IActionResult GetConfigurationPage()
    {
        try
        {
            var assembly = Assembly.GetExecutingAssembly();
            using var stream = assembly.GetManifestResourceStream("JellyfinOIDCPlugin.web.configurationpage.html");
            if (stream == null)
            {
                Console.WriteLine("OIDC: Configuration page resource not found");
                return NotFound("Configuration page resource not found");
            }

            using var reader = new StreamReader(stream);
            var content = reader.ReadToEnd();
            
            return Content(content, "text/html");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"OIDC: Error serving configuration page {ex}");
            return StatusCode(500, $"Error loading configuration page: {ex.Message}");
        }
    }
}
EOF
            cat > JellyfinOIDCPlugin.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <AssemblyName>JellyfinOIDCPlugin.v2</AssemblyName>
    <RootNamespace>JellyfinOIDCPlugin</RootNamespace>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <AssemblyVersion>1.0.2.0</AssemblyVersion>
    <FileVersion>1.0.2.0</FileVersion>
    <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Jellyfin.Controller" Version="10.11.5">
      <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
    <PackageReference Include="Jellyfin.Model" Version="10.11.5">
      <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
    <PackageReference Include="Jellyfin.Common" Version="10.11.5">
      <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
    <PackageReference Include="Jellyfin.Data" Version="10.11.5">
      <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
    <PackageReference Include="Jellyfin.Database.Implementations" Version="10.11.5">
      <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
    <PackageReference Include="IdentityModel.OidcClient" Version="5.2.1">
      <PrivateAssets>none</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.11">
      <ExcludeAssets>runtime</ExcludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="web\\*.html" />
    <EmbeddedResource Include="web\\*.js" />
    <EmbeddedResource Include="web\\*.css" />
  </ItemGroup>
</Project>
EOF
            dotnet restore
            dotnet publish -c Release --no-self-contained -o "${DIST_DIR}"
            cd "${DIST_DIR}"
            zip -r "${ART_DIR}/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip" .
          '''
        }
      }
    }
    stage('Push to Harbor') {
      steps {
        container('dotnet') {
          withCredentials([usernamePassword(credentialsId: 'harbor-robot', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')]) {
            sh '''
              set -euo pipefail
              WORKDIR="$(pwd)/build"
              ORAS_BIN="/usr/local/bin/oras"
              curl -sSL "https://github.com/oras-project/oras/releases/download/v${ORAS_VERSION}/oras_${ORAS_VERSION}_linux_amd64.tar.gz" | tar -xz -C /usr/local/bin oras
              ref_host="$(echo "${HARBOR_REPO}" | cut -d/ -f1)"
              "${ORAS_BIN}" login "${ref_host}" -u "${HARBOR_USERNAME}" -p "${HARBOR_PASSWORD}"
              artifact="${WORKDIR}/artifact/OIDC_Authentication_${PLUGIN_VERSION}-net9.zip"
              "${ORAS_BIN}" push "${HARBOR_REPO}:${JELLYFIN_VERSION}" "${artifact}:application/zip" --artifact-type application/zip
              "${ORAS_BIN}" push "${HARBOR_REPO}:latest" "${artifact}:application/zip" --artifact-type application/zip
            '''
          }
        }
      }
    }
  }
  post {
    always {
      container('dotnet') {
        archiveArtifacts artifacts: 'build/artifact/*.zip', allowEmptyArchive: true
      }
    }
  }
}
