using System.IO;
using System.Linq;
using System.Text;

namespace DragonISO.App.Services;

/// <summary>
/// User-editable template for the NDI source name a participant's ISO is
/// published as. Default <c>"{name}"</c> renders the speaker's display name
/// directly, which is what downstream switchers want when they key on
/// readable identifiers. Operators can override globally to
/// <c>"Dragon-ISO_{guid}"</c> for the legacy stable-id behavior, or
/// <c>"Dragon-ISO_{machine}_{name}"</c> when multiple Dragon-ISO machines feed
/// the same NDI network and you want the source name to carry both.
/// Per-participant overrides take priority over whatever template is set.
///
/// Tokens expanded in <see cref="Render"/>:
///   <c>{name}</c>      participant display name, sanitized (alphanumeric + underscore)
///   <c>{guid}</c>      first 8 hex chars of the participant's Id, uppercase
///   <c>{machine}</c>   sanitized PC hostname (Environment.MachineName)
///   <c>{timestamp}</c> current local time as <c>yyyyMMdd_HHmmss</c>
///
/// Empty-name fallback: if the rendered result is empty/whitespace (e.g.
/// template was <c>"{name}"</c> and the participant joined with no display
/// name yet), <see cref="Render"/> falls back to <c>Dragon-ISO_{guid}</c> so
/// the NDI sender always has a usable, unique identifier.
///
/// Persisted to <c>%LOCALAPPDATA%\Dragon-ISO\output-name-template.txt</c>.
/// </summary>
public static class OutputNameTemplate
{
    /// <summary>
    /// Default template â€” renders just the speaker's display name. Was
    /// <c>"Dragon-ISO_{guid}"</c> in pre-v1 builds; switched 2026-05-16 so
    /// new installs get human-readable source names out of the box.
    /// </summary>
    public const string DefaultTemplate = "{name}";

    /// <summary>
    /// Stable fallback used when the rendered template produces an empty
    /// string (typically because a participant has no display name yet).
    /// Mirrors the engine's legacy DefaultOutputName so the NDI sender is
    /// always uniquely identifiable.
    /// </summary>
    private const string EmptyNameFallback = "Dragon-ISO_{guid}";

    private static string TemplatePath =>
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Dragon-ISO", "output-name-template.txt");

    /// <summary>
    /// Get the operator's current template, or the shipped default when no
    /// override has been saved (or the override file is missing/unreadable).
    /// </summary>
    public static string Get()
    {
        try
        {
            if (File.Exists(TemplatePath))
            {
                var raw = File.ReadAllText(TemplatePath).Trim();
                if (!string.IsNullOrEmpty(raw)) return raw;
            }
        }
        catch
        {
            // Disk read failure â†’ fall through to default. The next Set() call
            // will overwrite cleanly.
        }
        return DefaultTemplate;
    }

    public static void Set(string template)
    {
        try
        {
            var dir = Path.GetDirectoryName(TemplatePath);
            if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
            File.WriteAllText(TemplatePath, template ?? string.Empty);
        }
        catch
        {
            // Best-effort persistence; the in-memory value still sticks for
            // this session.
        }
    }

    /// <summary>
    /// Expand tokens in <paramref name="template"/> for a specific participant.
    /// Result is sanitized into NDI-safe characters: alphanumeric, underscore,
    /// hyphen, period. NDI spec allows more, but a conservative set keeps
    /// downstream switchers happy.
    /// </summary>
    public static string Render(string template, Guid participantId, string displayName)
    {
        var safeName = SanitizeForNdi(displayName);
        var guid = participantId.ToString("N")[..8].ToUpperInvariant();
        var machine = SanitizeForNdi(Environment.MachineName);
        var timestamp = DateTimeOffset.Now.ToString("yyyyMMdd_HHmmss");

        var result = template
            .Replace("{name}", safeName)
            .Replace("{guid}", guid)
            .Replace("{machine}", machine)
            .Replace("{timestamp}", timestamp);

        // Final sanitize on the rendered result â€” protects against a template
        // that includes literal characters NDI doesn't accept.
        var sanitized = SanitizeForNdi(result);

        // Empty-name fallback. The default template "{name}" can render to
        // an unusable result for participants whose DisplayName hasn't been
        // populated yet (Teams sometimes delivers the displayName a tick
        // after the participant join event). Two failure modes to catch:
        //
        //   â€¢ DisplayName == "" â†’ "{name}" expands to "" â†’ sanitized "".
        //   â€¢ DisplayName == "   " â†’ "{name}" expands to "___" because the
        //     sanitizer converts whitespace to underscores.
        //
        // Neither is a meaningful NDI source identifier, so we substitute
        // Dragon-ISO_{guid}. The Any(char.IsLetterOrDigit) check covers both
        // cases â€” anything without at least one alphanumeric is unusable.
        // We apply this AFTER token expansion (not on the raw input) so a
        // template like "PFX_{name}" with empty displayName still works:
        // it renders to "PFX_" which contains alphanumerics and is left
        // alone.
        if (string.IsNullOrWhiteSpace(sanitized) || !sanitized.Any(char.IsLetterOrDigit))
        {
            sanitized = SanitizeForNdi(EmptyNameFallback.Replace("{guid}", guid));
        }

        return sanitized;
    }

    private static string SanitizeForNdi(string s)
    {
        if (string.IsNullOrEmpty(s)) return string.Empty;
        var sb = new StringBuilder(s.Length);
        foreach (var c in s)
        {
            if (char.IsLetterOrDigit(c) || c is '_' or '-' or '.')
                sb.Append(c);
            else if (char.IsWhiteSpace(c))
                sb.Append('_');
            // else: skip
        }
        return sb.ToString();
    }
}
