using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Domain.Content;
using Tiku.Infrastructure.Security;

namespace Tiku.Infrastructure.Content;

public sealed partial class ContentManagementService
{
    public async Task<CatalogList<ContentNodeManagementItem>> GetNodesAsync(
        ContentManagementActor actor,
        ContentManagementFilter filter,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        if (!filter.EntryId.HasValue) throw new ContentManagementException("entryId is required.", "entry_id_required");

        await AssertEntryAsync(actor, scope, filter.EntryId, cancellationToken);
        var regionIds = scope.RegionIds.ToArray();
        var query = dbContext.ContentNodes
            .AsNoTracking()
            .Where(node => node.TenantId == actor.TenantId && node.EntryId == filter.EntryId.Value)
            .ApplyDataScope(
                scope,
                node => node.CreatedBy == actor.UserId,
                node => node.RegionId.HasValue && regionIds.Contains(node.RegionId.Value));

        if (!filter.IncludeInactive) query = query.Where(node => node.IsActive);

        if (filter.RegionId.HasValue) query = query.Where(node => node.RegionId == filter.RegionId.Value);

        if (filter.ParentId is not null)
        {
            if (string.Equals(filter.ParentId, "root", StringComparison.OrdinalIgnoreCase))
                query = query.Where(node => node.ParentId == null);
            else if (Guid.TryParse(filter.ParentId, out var parentId))
                query = query.Where(node => node.ParentId == parentId);
        }

        if (TryParse(filter.MarkerType, out ContentMarkerType markerType))
            query = query.Where(node => node.MarkerType == markerType);

        if (!string.IsNullOrWhiteSpace(filter.Keyword))
        {
            var keyword = filter.Keyword.Trim();
            query = query.Where(node =>
                node.Name.Contains(keyword) ||
                (node.NodeKey != null && node.NodeKey.Contains(keyword)));
        }

        query = string.Equals(filter.Mode, "flat", StringComparison.OrdinalIgnoreCase)
            ? query.OrderBy(node => node.Path).ThenBy(node => node.SortOrder)
            : query.OrderBy(node => node.SortOrder).ThenBy(node => node.CreatedAt);

        var items = await query
            .Take(ResolveLimit(filter.Limit))
            .Select(node => ToNodeItem(node))
            .ToArrayAsync(cancellationToken);

        return new CatalogList<ContentNodeManagementItem>(items);
    }

    public async Task<ContentManagementResult<ContentNodeManagementItem>> UpsertNodeAsync(
        ContentManagementActor actor,
        UpsertContentNodeCommand command,
        CancellationToken cancellationToken = default)
    {
        var scope = await RequireDataScopeAsync(actor, cancellationToken);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.Name);
        await AssertEntryAsync(actor, scope, command.EntryId, cancellationToken);
        await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken);
        await AssertNodeAsync(actor, scope, command.ParentId, cancellationToken);

        var nodeKey = Normalize(command.NodeKey) ??
                      Normalize(command.Id?.ToString("N")) ??
                      Guid.NewGuid().ToString("N");
        var node = await ResolveEntityAsync(
            dbContext.ContentNodes,
            actor.TenantId,
            command.Id,
            item => item.EntryId == command.EntryId && item.NodeKey == nodeKey,
            cancellationToken);

        var isNew = node is null;
        if (command.Id.HasValue && (node is null || node.Id != command.Id.Value))
            throw new ContentManagementException("Content node was not found.", "node_not_found");

        if (node is not null && !scope.AllowsResource(actor.UserId, node.CreatedBy, node.RegionId))
            throw new ContentManagementException("Content node was not found.", "node_not_found");

        if (node is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId))
            throw new ContentManagementException("Content node was not found.", "node_not_found");

        node ??= new ContentNode
        {
            Id = command.Id ?? Guid.NewGuid(),
            TenantId = actor.TenantId,
            EntryId = command.EntryId,
            NodeKey = nodeKey,
            CreatedBy = actor.UserId
        };

        var path = await BuildNodePathAsync(actor.TenantId, command.EntryId, node.Id, command.ParentId,
            cancellationToken);
        node.EntryId = command.EntryId;
        node.RegionId = command.RegionId;
        node.ParentId = command.ParentId;
        node.LegacyId = Normalize(command.LegacyId);
        node.Name = command.Name.Trim();
        node.NodeType = Parse(command.NodeType, ContentNodeType.Category, "node_type_invalid");
        node.MarkerType = ParseNullable<ContentMarkerType>(command.MarkerType, "marker_type_invalid");
        node.MarkerConfig = JsonObjectOrDefault(command.MarkerConfig);
        node.Path = path.Path;
        node.Depth = path.Depth;
        node.SortOrder = command.Order ?? 0;
        node.IsActive = command.IsActive ?? true;
        node.IsSelectable = command.IsSelectable ?? true;
        node.IsLeaf = command.IsLeaf ?? false;
        node.AccessRules = JsonObjectOrDefault(command.AccessRules);
        node.Metadata = JsonObjectOrDefault(command.Metadata);
        node.UpdatedBy = actor.UserId;

        if (isNew) dbContext.ContentNodes.Add(node);

        if (command.ParentId.HasValue)
        {
            var parent = await dbContext.ContentNodes.SingleOrDefaultAsync(
                item => item.TenantId == actor.TenantId && item.Id == command.ParentId.Value,
                cancellationToken);
            if (parent is not null) parent.IsLeaf = false;
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        return new ContentManagementResult<ContentNodeManagementItem>(ToNodeItem(node));
    }
}