using System.Security.Cryptography;
using System.Text;
using LibGit2Sharp;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OTSSignsOrchestrator.Core.Configuration;
using OTSSignsOrchestrator.Core.Models.DTOs;

namespace OTSSignsOrchestrator.Core.Services;

/// <summary>
/// Fetches template.yml and template.env from a Git repository using LibGit2Sharp.
/// Caches clones on disk, keyed by SHA-256 hash of the repo URL.
/// </summary>
public class GitTemplateService
{
    private readonly GitOptions _options;
    private readonly ILogger<GitTemplateService> _logger;

    public GitTemplateService(IOptions<GitOptions> options, ILogger<GitTemplateService> logger)
    {
        _options = options.Value;
        _logger = logger;
    }

    public async Task<TemplateConfig> FetchAsync(string repoUrl, string? pat = null, bool forceRefresh = false)
    {
        var cacheKey = ComputeCacheKey(repoUrl);
        var cacheDir = Path.Combine(_options.CacheDir, cacheKey);

        _logger.LogInformation("Fetching templates from repo (cacheKey={CacheKey}, force={Force})", cacheKey, forceRefresh);

        await Task.Run(() =>
        {
            if (!Directory.Exists(cacheDir) || !Repository.IsValid(cacheDir))
            {
                CloneRepo(repoUrl, pat, cacheDir);
            }
            else if (forceRefresh || IsCacheStale(cacheDir))
            {
                FetchLatest(repoUrl, pat, cacheDir);
            }
            else
            {
                _logger.LogDebug("Using cached clone at {CacheDir}", cacheDir);
            }
        });

        var yamlPath = FindFile(cacheDir, "template.yml");

        if (yamlPath == null)
            throw new FileNotFoundException("template.yml not found in repository root. Commit the template file produced by ComposeRenderService.GetTemplateYaml() to the repo root.");

        var yaml = await File.ReadAllTextAsync(yamlPath);

        return new TemplateConfig
        {
            Yaml = yaml,
            FetchedAt = DateTime.UtcNow,
            CacheDir = cacheDir,
        };
    }

    private void CloneRepo(string repoUrl, string? pat, string cacheDir)
    {
        _logger.LogInformation("Shallow cloning repo to {CacheDir}", cacheDir);

        if (Directory.Exists(cacheDir))
            Directory.Delete(cacheDir, recursive: true);

        Directory.CreateDirectory(cacheDir);

        var cloneOpts = new CloneOptions { IsBare = false, RecurseSubmodules = false };

        if (!string.IsNullOrEmpty(pat))
        {
            cloneOpts.FetchOptions.CredentialsProvider = (_, _, _) =>
                new UsernamePasswordCredentials { Username = pat, Password = string.Empty };
        }

        try
        {
            Repository.Clone(repoUrl, cacheDir, cloneOpts);
        }
        catch (LibGit2SharpException ex)
        {
            _logger.LogError(ex, "Git clone failed for repo (cacheKey={CacheKey})", ComputeCacheKey(repoUrl));
            throw new InvalidOperationException($"Failed to clone repository. Check URL and credentials. Error: {ex.Message}", ex);
        }
    }

    private void FetchLatest(string repoUrl, string? pat, string cacheDir)
    {
        _logger.LogInformation("Fetching latest from origin in {CacheDir}", cacheDir);

        try
        {
            using var repo = new Repository(cacheDir);

            var fetchOpts = new FetchOptions();
            if (!string.IsNullOrEmpty(pat))
            {
                fetchOpts.CredentialsProvider = (_, _, _) =>
                    new UsernamePasswordCredentials { Username = pat, Password = string.Empty };
            }

            var remote = repo.Network.Remotes["origin"];
            var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification).ToArray();
            Commands.Fetch(repo, "origin", refSpecs, fetchOpts, "Auto-fetch for template update");

            var trackingBranch = repo.Head.TrackedBranch;
            if (trackingBranch != null)
            {
                repo.Reset(ResetMode.Hard, trackingBranch.Tip);
            }

            WriteCacheTimestamp(cacheDir);
        }
        catch (LibGit2SharpException ex)
        {
            _logger.LogError(ex, "Git fetch failed for repo at {CacheDir}", cacheDir);
            throw new InvalidOperationException($"Failed to fetch latest templates. Error: {ex.Message}", ex);
        }
    }

    private bool IsCacheStale(string cacheDir)
    {
        var stampFile = Path.Combine(cacheDir, ".fetch-timestamp");
        if (!File.Exists(stampFile)) return true;

        if (DateTime.TryParse(File.ReadAllText(stampFile), out var lastFetch))
        {
            return (DateTime.UtcNow - lastFetch).TotalMinutes > _options.CacheTtlMinutes;
        }
        return true;
    }

    private static void WriteCacheTimestamp(string cacheDir)
    {
        var stampFile = Path.Combine(cacheDir, ".fetch-timestamp");
        File.WriteAllText(stampFile, DateTime.UtcNow.ToString("O"));
    }

    private static string? FindFile(string dir, string fileName)
    {
        var path = Path.Combine(dir, fileName);
        return File.Exists(path) ? path : null;
    }

    private static string ComputeCacheKey(string repoUrl)
    {
        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(repoUrl));
        return Convert.ToHexString(hash)[..16].ToLowerInvariant();
    }
}
