using ColaFlow.Modules.Mcp.Application.DTOs;
using ColaFlow.Modules.Mcp.Domain.Entities;
using ColaFlow.Modules.Mcp.Domain.Exceptions;
using ColaFlow.Modules.Mcp.Domain.Repositories;
using ColaFlow.Modules.Mcp.Domain.ValueObjects;
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace ColaFlow.Modules.Mcp.Application.Services;

/// <summary>
/// Service implementation for PendingChange management
/// </summary>
public class PendingChangeService : IPendingChangeService
{
    private readonly IPendingChangeRepository _repository;
    private readonly ITenantContext _tenantContext;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IPublisher _publisher;
    private readonly ILogger<PendingChangeService> _logger;

    public PendingChangeService(
        IPendingChangeRepository repository,
        ITenantContext tenantContext,
        IHttpContextAccessor httpContextAccessor,
        IPublisher publisher,
        ILogger<PendingChangeService> logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        _publisher = publisher ?? throw new ArgumentNullException(nameof(publisher));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task<PendingChangeDto> CreateAsync(
        CreatePendingChangeRequest request,
        CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.GetCurrentTenantId();

        // Get API Key ID from HttpContext (set by MCP authentication middleware)
        var apiKeyIdNullable = _httpContextAccessor.HttpContext?.Items["ApiKeyId"] as Guid?;
        if (!apiKeyIdNullable.HasValue)
        {
            throw new McpUnauthorizedException("API Key not found in request context");
        }
        var apiKeyId = apiKeyIdNullable.Value;

        _logger.LogInformation(
            "Creating PendingChange: Tool={ToolName}, Operation={Operation}, EntityType={EntityType}, Tenant={TenantId}",
            request.ToolName, request.Diff.Operation, request.Diff.EntityType, tenantId);

        // Create PendingChange entity
        var pendingChange = PendingChange.Create(
            request.ToolName,
            request.Diff,
            tenantId,
            apiKeyId,
            request.ExpirationHours);

        // Save to database
        await _repository.AddAsync(pendingChange, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        // Publish domain events
        foreach (var domainEvent in pendingChange.DomainEvents)
        {
            await _publisher.Publish(domainEvent, cancellationToken);
        }
        pendingChange.ClearDomainEvents();

        _logger.LogInformation(
            "PendingChange created: {Id}, ExpiresAt={ExpiresAt}",
            pendingChange.Id, pendingChange.ExpiresAt);

        return MapToDto(pendingChange);
    }

    public async Task<PendingChangeDto?> GetByIdAsync(
        Guid id,
        CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.GetCurrentTenantId();

        var pendingChange = await _repository.GetByIdAsync(id, cancellationToken);
        if (pendingChange == null)
        {
            return null;
        }

        // Verify tenant isolation
        if (pendingChange.TenantId != tenantId)
        {
            _logger.LogWarning(
                "Attempted cross-tenant access: PendingChange {Id} belongs to Tenant {OwnerId}, but requested by Tenant {RequesterId}",
                id, pendingChange.TenantId, tenantId);
            return null;
        }

        return MapToDto(pendingChange);
    }

    public async Task<(List<PendingChangeDto> Items, int TotalCount)> GetPendingChangesAsync(
        PendingChangeFilterDto filter,
        CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.GetCurrentTenantId();

        // Build query
        var query = (await _repository.GetByTenantAsync(tenantId, cancellationToken))
            .AsEnumerable();

        // Apply filters
        if (filter.Status.HasValue)
        {
            query = query.Where(x => x.Status == filter.Status.Value);
        }

        if (!string.IsNullOrWhiteSpace(filter.EntityType))
        {
            query = query.Where(x => x.Diff.EntityType == filter.EntityType);
        }

        if (filter.EntityId.HasValue)
        {
            query = query.Where(x => x.Diff.EntityId == filter.EntityId.Value);
        }

        if (filter.ApiKeyId.HasValue)
        {
            query = query.Where(x => x.ApiKeyId == filter.ApiKeyId.Value);
        }

        if (!string.IsNullOrWhiteSpace(filter.ToolName))
        {
            query = query.Where(x => x.ToolName == filter.ToolName);
        }

        if (filter.IncludeExpired == false)
        {
            query = query.Where(x => x.Status != PendingChangeStatus.Expired);
        }

        // Get total count before pagination
        var totalCount = query.Count();

        // Apply pagination
        var items = query
            .OrderByDescending(x => x.CreatedAt)
            .Skip((filter.Page - 1) * filter.PageSize)
            .Take(filter.PageSize)
            .Select(MapToDto)
            .ToList();

        _logger.LogInformation(
            "Retrieved {Count}/{Total} PendingChanges for Tenant {TenantId} (Page {Page}/{PageSize})",
            items.Count, totalCount, tenantId, filter.Page, filter.PageSize);

        return (items, totalCount);
    }

    public async Task ApproveAsync(
        Guid pendingChangeId,
        Guid approvedBy,
        CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.GetCurrentTenantId();

        var pendingChange = await _repository.GetByIdAsync(pendingChangeId, cancellationToken);
        if (pendingChange == null)
        {
            throw new McpNotFoundException("PendingChange", pendingChangeId.ToString());
        }

        // Verify tenant isolation
        if (pendingChange.TenantId != tenantId)
        {
            throw new McpForbiddenException(
                $"Cannot approve PendingChange from different tenant");
        }

        _logger.LogInformation(
            "Approving PendingChange {Id} by User {UserId}",
            pendingChangeId, approvedBy);

        // Domain method validates business rules and raises PendingChangeApprovedEvent
        pendingChange.Approve(approvedBy);

        await _repository.UpdateAsync(pendingChange, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        // Publish domain events (will trigger operation execution)
        foreach (var domainEvent in pendingChange.DomainEvents)
        {
            await _publisher.Publish(domainEvent, cancellationToken);
        }
        pendingChange.ClearDomainEvents();

        _logger.LogInformation(
            "PendingChange {Id} approved successfully",
            pendingChangeId);
    }

    public async Task RejectAsync(
        Guid pendingChangeId,
        Guid rejectedBy,
        string reason,
        CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.GetCurrentTenantId();

        var pendingChange = await _repository.GetByIdAsync(pendingChangeId, cancellationToken);
        if (pendingChange == null)
        {
            throw new McpNotFoundException("PendingChange", pendingChangeId.ToString());
        }

        // Verify tenant isolation
        if (pendingChange.TenantId != tenantId)
        {
            throw new McpForbiddenException(
                $"Cannot reject PendingChange from different tenant");
        }

        _logger.LogInformation(
            "Rejecting PendingChange {Id} by User {UserId} - Reason: {Reason}",
            pendingChangeId, rejectedBy, reason);

        // Domain method validates business rules and raises PendingChangeRejectedEvent
        pendingChange.Reject(rejectedBy, reason);

        await _repository.UpdateAsync(pendingChange, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        // Publish domain events
        foreach (var domainEvent in pendingChange.DomainEvents)
        {
            await _publisher.Publish(domainEvent, cancellationToken);
        }
        pendingChange.ClearDomainEvents();

        _logger.LogInformation(
            "PendingChange {Id} rejected successfully",
            pendingChangeId);
    }

    public async Task MarkAsAppliedAsync(
        Guid pendingChangeId,
        string result,
        CancellationToken cancellationToken = default)
    {
        var pendingChange = await _repository.GetByIdAsync(pendingChangeId, cancellationToken);
        if (pendingChange == null)
        {
            throw new McpNotFoundException("PendingChange", pendingChangeId.ToString());
        }

        _logger.LogInformation(
            "Marking PendingChange {Id} as Applied - Result: {Result}",
            pendingChangeId, result);

        // Domain method validates business rules and raises PendingChangeAppliedEvent
        pendingChange.MarkAsApplied(result);

        await _repository.UpdateAsync(pendingChange, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        // Publish domain events
        foreach (var domainEvent in pendingChange.DomainEvents)
        {
            await _publisher.Publish(domainEvent, cancellationToken);
        }
        pendingChange.ClearDomainEvents();

        _logger.LogInformation(
            "PendingChange {Id} marked as Applied",
            pendingChangeId);
    }

    public async Task<int> ExpireOldChangesAsync(CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Starting expiration check for old PendingChanges");

        var expiredChanges = await _repository.GetExpiredAsync(cancellationToken);

        var count = 0;
        foreach (var change in expiredChanges)
        {
            try
            {
                change.Expire();
                await _repository.UpdateAsync(change, cancellationToken);

                // Publish domain events
                foreach (var domainEvent in change.DomainEvents)
                {
                    await _publisher.Publish(domainEvent, cancellationToken);
                }
                change.ClearDomainEvents();

                count++;

                _logger.LogWarning(
                    "PendingChange expired: {Id} - {ToolName} {Operation} {EntityType}",
                    change.Id, change.ToolName, change.Diff.Operation, change.Diff.EntityType);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,
                    "Failed to expire PendingChange {Id}",
                    change.Id);
            }
        }

        if (count > 0)
        {
            await _repository.SaveChangesAsync(cancellationToken);
        }

        _logger.LogInformation(
            "Expired {Count} PendingChanges",
            count);

        return count;
    }

    public async Task DeleteAsync(
        Guid pendingChangeId,
        CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.GetCurrentTenantId();

        var pendingChange = await _repository.GetByIdAsync(pendingChangeId, cancellationToken);
        if (pendingChange == null)
        {
            throw new McpNotFoundException("PendingChange", pendingChangeId.ToString());
        }

        // Verify tenant isolation
        if (pendingChange.TenantId != tenantId)
        {
            throw new McpForbiddenException(
                $"Cannot delete PendingChange from different tenant");
        }

        // Only allow deletion of Expired or Rejected changes
        if (pendingChange.Status != PendingChangeStatus.Expired &&
            pendingChange.Status != PendingChangeStatus.Rejected)
        {
            throw new McpValidationException(
                $"Can only delete PendingChanges with Expired or Rejected status. Current status: {pendingChange.Status}");
        }

        _logger.LogInformation(
            "Deleting PendingChange {Id} (Status: {Status})",
            pendingChangeId, pendingChange.Status);

        await _repository.DeleteAsync(pendingChange, cancellationToken);
        await _repository.SaveChangesAsync(cancellationToken);

        _logger.LogInformation(
            "PendingChange {Id} deleted successfully",
            pendingChangeId);
    }

    private static PendingChangeDto MapToDto(PendingChange pendingChange)
    {
        return new PendingChangeDto
        {
            Id = pendingChange.Id,
            TenantId = pendingChange.TenantId,
            ApiKeyId = pendingChange.ApiKeyId,
            ToolName = pendingChange.ToolName,
            Diff = new DiffPreviewDto
            {
                Operation = pendingChange.Diff.Operation,
                EntityType = pendingChange.Diff.EntityType,
                EntityId = pendingChange.Diff.EntityId,
                EntityKey = pendingChange.Diff.EntityKey,
                BeforeData = pendingChange.Diff.BeforeData,
                AfterData = pendingChange.Diff.AfterData,
                ChangedFields = pendingChange.Diff.ChangedFields.Select(f => new DiffFieldDto
                {
                    FieldName = f.FieldName,
                    DisplayName = f.DisplayName,
                    OldValue = f.OldValue,
                    NewValue = f.NewValue,
                    DiffHtml = f.DiffHtml
                }).ToList()
            },
            Status = pendingChange.Status.ToString(),
            CreatedAt = pendingChange.CreatedAt,
            ExpiresAt = pendingChange.ExpiresAt,
            ApprovedBy = pendingChange.ApprovedBy,
            ApprovedAt = pendingChange.ApprovedAt,
            RejectedBy = pendingChange.RejectedBy,
            RejectedAt = pendingChange.RejectedAt,
            RejectionReason = pendingChange.RejectionReason,
            AppliedAt = pendingChange.AppliedAt,
            ApplicationResult = pendingChange.ApplicationResult,
            IsExpired = pendingChange.IsExpired(),
            CanBeApproved = pendingChange.CanBeApproved(),
            CanBeRejected = pendingChange.CanBeRejected(),
            Summary = pendingChange.GetSummary()
        };
    }
}
