using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using Microsoft.EntityFrameworkCore;

namespace ClaudeDo.Worker.Skills;

public sealed class SessionSkillRegistry : ISessionSkillRegistry
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly IRepoCloner _cloner;
    private readonly string _skillsRoot;

    public SessionSkillRegistry(
        IDbContextFactory<ClaudeDoDbContext> dbFactory,
        IRepoCloner cloner,
        string? skillsRoot = null)
    {
        _dbFactory = dbFactory;
        _cloner = cloner;
        _skillsRoot = skillsRoot ?? Path.Combine(Paths.AppDataRoot(), "session-skills");
    }

    public async Task<IReadOnlyList<string>> InstallAsync(string url, CancellationToken ct)
    {
        var tempDir = Path.Combine(Path.GetTempPath(), $"claudedo_skill_clone_{Guid.NewGuid():N}");
        try
        {
            var cloned = await _cloner.CloneAsync(url, tempDir, ct);
            var discovered = DiscoverSkills(cloned.LocalPath);

            await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
            var repo = new SessionSkillRepository(ctx);

            foreach (var skill in discovered)
            {
                var existing = await repo.GetAsync(skill.Name, ct);
                if (existing is not null && existing.SourceUrl != url)
                {
                    throw new InvalidOperationException(
                        $"Skill '{skill.Name}' is already installed from a different source ('{existing.SourceUrl}'). " +
                        "Remove it before installing from a new source.");
                }
            }

            var installedNames = new List<string>();
            foreach (var skill in discovered)
            {
                InstallSkillFiles(skill, url, cloned.PinnedRef, out var entity);
                await repo.UpsertAsync(entity, ct);
                installedNames.Add(skill.Name);
            }

            return installedNames;
        }
        finally
        {
            TryDeleteDirectory(tempDir);
        }
    }

    public async Task UpdateAsync(string sourceUrl, CancellationToken ct)
    {
        var tempDir = Path.Combine(Path.GetTempPath(), $"claudedo_skill_clone_{Guid.NewGuid():N}");
        try
        {
            var cloned = await _cloner.CloneAsync(sourceUrl, tempDir, ct);
            var discovered = DiscoverSkills(cloned.LocalPath);
            var discoveredNames = discovered.Select(s => s.Name).ToHashSet();

            await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
            var repo = new SessionSkillRepository(ctx);

            var existingRows = await repo.ListBySourceAsync(sourceUrl, ct);
            foreach (var stale in existingRows.Where(r => !discoveredNames.Contains(r.Name)))
            {
                TryDeleteDirectory(Path.Combine(_skillsRoot, stale.Name));
                await repo.DeleteAsync(stale.Name, ct);
            }

            foreach (var skill in discovered)
            {
                InstallSkillFiles(skill, sourceUrl, cloned.PinnedRef, out var entity);
                await repo.UpsertAsync(entity, ct);
            }
        }
        finally
        {
            TryDeleteDirectory(tempDir);
        }
    }

    public async Task RemoveAsync(string sourceUrl, CancellationToken ct)
    {
        await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
        var repo = new SessionSkillRepository(ctx);

        var rows = await repo.ListBySourceAsync(sourceUrl, ct);
        foreach (var row in rows)
            TryDeleteDirectory(Path.Combine(_skillsRoot, row.Name));

        await repo.DeleteBySourceAsync(sourceUrl, ct);
    }

    public async Task<IReadOnlyList<SessionSkillEntity>> ListAsync(CancellationToken ct)
    {
        await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
        return await new SessionSkillRepository(ctx).ListAsync(ct);
    }

    private sealed record DiscoveredSkill(string Name, string Description, string SourceDir, string Subpath);

    private static List<DiscoveredSkill> DiscoverSkills(string cloneRoot)
    {
        var skillsDir = Path.Combine(cloneRoot, "skills");
        var discovered = new List<DiscoveredSkill>();

        if (Directory.Exists(skillsDir))
        {
            foreach (var dir in Directory.EnumerateDirectories(skillsDir).OrderBy(d => d, StringComparer.Ordinal))
            {
                var skillMd = Path.Combine(dir, "SKILL.md");
                if (!File.Exists(skillMd)) continue;

                var frontmatter = SkillFrontmatter.Parse(skillMd);
                var dirName = Path.GetFileName(dir);
                discovered.Add(new DiscoveredSkill(frontmatter.Name, frontmatter.Description, dir, $"skills/{dirName}"));
            }

            if (discovered.Count > 0)
                return discovered;
        }

        var rootSkillMd = Path.Combine(cloneRoot, "SKILL.md");
        if (File.Exists(rootSkillMd))
        {
            var frontmatter = SkillFrontmatter.Parse(rootSkillMd);
            discovered.Add(new DiscoveredSkill(frontmatter.Name, frontmatter.Description, cloneRoot, "."));
            return discovered;
        }

        throw new InvalidOperationException(
            "No skills found: expected either a top-level 'skills/<name>/SKILL.md' layout or a root 'SKILL.md'.");
    }

    private void InstallSkillFiles(DiscoveredSkill skill, string sourceUrl, string pinnedRef, out SessionSkillEntity entity)
    {
        var destDir = Path.Combine(_skillsRoot, skill.Name);
        if (Directory.Exists(destDir))
            Directory.Delete(destDir, recursive: true);
        Directory.CreateDirectory(destDir);

        CopyFlat(skill.SourceDir, destDir);

        entity = new SessionSkillEntity
        {
            Name = skill.Name,
            SourceUrl = sourceUrl,
            PinnedRef = pinnedRef,
            Subpath = skill.Subpath,
            Description = skill.Description,
            AddedAt = DateTimeOffset.UtcNow,
        };
    }

    private static void CopyFlat(string sourceDir, string destDir)
    {
        foreach (var file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
        {
            var relative = Path.GetRelativePath(sourceDir, file);
            if (relative.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
                    .Contains(".git", StringComparer.Ordinal))
                continue;

            var destPath = Path.Combine(destDir, relative);
            var destParent = Path.GetDirectoryName(destPath);
            if (!string.IsNullOrEmpty(destParent))
                Directory.CreateDirectory(destParent);
            File.Copy(file, destPath, overwrite: true);
        }
    }

    private static void TryDeleteDirectory(string dir)
    {
        try
        {
            if (Directory.Exists(dir))
                Directory.Delete(dir, recursive: true);
        }
        catch { /* best effort */ }
    }
}
