using System.Diagnostics;
using Scry.Core.Data;
using Scry.Core.Imaging;
using Scry.Core.Models;
using SkiaSharp;

namespace Scry.Core.Recognition;

public class CardRecognitionService : IDisposable
{
    private readonly CardDatabase _database;
    private List<Card>? _cardCache;
    private readonly SemaphoreSlim _cacheLock = new(1, 1);
    
    private const int ColorHashBits = 192;
    private const int MatchThreshold = 25;
    private const float MinConfidence = 0.85f;
    
    /// <summary>
    /// Enable card detection and perspective correction.
    /// When disabled, assumes the input image is already a cropped card.
    /// </summary>
    public bool EnableCardDetection { get; set; } = true;
    
    /// <summary>
    /// Try multiple rotations (0°, 90°, 180°, 270°) when matching.
    /// Useful when card orientation is unknown.
    /// </summary>
    public bool EnableRotationMatching { get; set; } = true;
    
    public CardRecognitionService(CardDatabase database)
    {
        _database = database;
    }
    
    public async Task<ScanResult> RecognizeAsync(Stream imageStream, CancellationToken ct = default)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            using var bitmap = SKBitmap.Decode(imageStream);
            if (bitmap == null)
            {
                return ScanResult.Failed("Could not decode image");
            }
            
            return await RecognizeAsync(bitmap, ct);
        }
        catch (Exception ex)
        {
            return ScanResult.Failed($"Recognition error: {ex.Message}");
        }
        finally
        {
            stopwatch.Stop();
        }
    }
    
    public async Task<ScanResult> RecognizeAsync(SKBitmap bitmap, CancellationToken ct = default)
    {
        var stopwatch = Stopwatch.StartNew();
        
        try
        {
            var cards = await GetCardCacheAsync(ct);
            Console.WriteLine($"[Scry] Database has {cards.Count} cards with hashes");
            
            if (cards.Count == 0)
            {
                return ScanResult.Failed("No cards in database. Run sync first.");
            }
            
            // Step 1: Detect and extract card from image (if enabled)
            SKBitmap cardImage;
            bool cardDetected = false;
            
            if (EnableCardDetection)
            {
                var detection = CardDetector.DetectCard(bitmap);
                if (detection.Found)
                {
                    cardImage = PerspectiveCorrection.WarpPerspective(bitmap, detection.Corners);
                    cardDetected = true;
                    Console.WriteLine($"[Scry] Card detected with confidence {detection.Confidence:P0}");
                }
                else
                {
                    // Fall back to using the whole image
                    Console.WriteLine($"[Scry] Card detection failed: {detection.DebugMessage}, using full image");
                    cardImage = bitmap;
                }
            }
            else
            {
                cardImage = bitmap;
            }
            
            try
            {
                // Step 2: Try matching with rotation variants (if enabled)
                var bestMatch = EnableRotationMatching
                    ? await FindBestMatchWithRotationsAsync(cardImage, cards, ct)
                    : FindBestMatchSingle(cardImage, cards);
                
                stopwatch.Stop();
                
                if (bestMatch == null)
                {
                    return ScanResult.Failed($"No match found (detection={cardDetected})");
                }
                
                var (matchedCard, distance, rotation) = bestMatch.Value;
                var confidence = PerceptualHash.CalculateConfidence(distance, ColorHashBits);
                Console.WriteLine($"[Scry] Best match: {matchedCard.Name}, distance={distance}, confidence={confidence:P0}, rotation={rotation}°");
                
                if (confidence < MinConfidence)
                {
                    return ScanResult.Failed($"Match confidence too low: {confidence:P0}");
                }
                
                return ScanResult.Matched(matchedCard, confidence, distance, stopwatch.Elapsed);
            }
            finally
            {
                // Dispose card image only if we created a new one
                if (cardDetected && cardImage != bitmap)
                {
                    cardImage.Dispose();
                }
            }
        }
        catch (Exception ex)
        {
            stopwatch.Stop();
            return ScanResult.Failed($"Recognition error: {ex.Message}");
        }
    }
    
    /// <summary>
    /// Compute hash for a card image with the full preprocessing pipeline.
    /// </summary>
    public byte[] ComputeHash(SKBitmap bitmap, bool applyPreprocessing = true)
    {
        if (applyPreprocessing)
        {
            // CLAHE is applied as the last step before hashing
            using var preprocessed = ImagePreprocessor.ApplyClahe(bitmap);
            return PerceptualHash.ComputeColorHash(preprocessed);
        }
        
        return PerceptualHash.ComputeColorHash(bitmap);
    }
    
    /// <summary>
    /// Compute hash for a reference image (used when building the database).
    /// Reference images are assumed to be already cropped and oriented correctly.
    /// </summary>
    public byte[] ComputeReferenceHash(SKBitmap bitmap)
    {
        using var preprocessed = ImagePreprocessor.ApplyClahe(bitmap);
        return PerceptualHash.ComputeColorHash(preprocessed);
    }
    
    public async Task InvalidateCacheAsync()
    {
        await _cacheLock.WaitAsync();
        try
        {
            _cardCache = null;
        }
        finally
        {
            _cacheLock.Release();
        }
    }
    
    private async Task<List<Card>> GetCardCacheAsync(CancellationToken ct)
    {
        if (_cardCache != null)
            return _cardCache;
        
        await _cacheLock.WaitAsync(ct);
        try
        {
            _cardCache ??= await _database.GetCardsWithHashAsync(ct);
            return _cardCache;
        }
        finally
        {
            _cacheLock.Release();
        }
    }
    
    /// <summary>
    /// Find best match trying all 4 rotations (0°, 90°, 180°, 270°).
    /// </summary>
    private Task<(Card Card, int Distance, int Rotation)?> FindBestMatchWithRotationsAsync(
        SKBitmap cardImage, 
        List<Card> candidates,
        CancellationToken ct)
    {
        return Task.Run(() =>
        {
            Card? bestMatch = null;
            var bestDistance = int.MaxValue;
            var bestRotation = 0;
            
            var rotations = new[] { 0, 90, 180, 270 };
            
            foreach (var rotation in rotations)
            {
                ct.ThrowIfCancellationRequested();
                
                using var rotated = rotation == 0 ? null : RotateImage(cardImage, rotation);
                var imageToHash = rotated ?? cardImage;
                
                // Apply CLAHE and compute hash
                using var preprocessed = ImagePreprocessor.ApplyClahe(imageToHash);
                var queryHash = PerceptualHash.ComputeColorHash(preprocessed);
                
                // Find best match for this rotation
                foreach (var candidate in candidates)
                {
                    if (candidate.Hash == null || candidate.Hash.Length != queryHash.Length)
                        continue;
                    
                    var distance = PerceptualHash.HammingDistance(queryHash, candidate.Hash);
                    
                    if (distance < bestDistance)
                    {
                        bestDistance = distance;
                        bestMatch = candidate;
                        bestRotation = rotation;
                    }
                    
                    // Early exit on perfect match
                    if (distance == 0 && bestMatch != null)
                        return (bestMatch, bestDistance, bestRotation);
                }
            }
            
            if (bestMatch == null || bestDistance > MatchThreshold)
                return null;
            
            return ((Card Card, int Distance, int Rotation)?)(bestMatch, bestDistance, bestRotation);
        }, ct);
    }
    
    /// <summary>
    /// Find best match without rotation (single orientation).
    /// </summary>
    private (Card Card, int Distance, int Rotation)? FindBestMatchSingle(
        SKBitmap cardImage,
        List<Card> candidates)
    {
        // Apply CLAHE and compute hash
        using var preprocessed = ImagePreprocessor.ApplyClahe(cardImage);
        var queryHash = PerceptualHash.ComputeColorHash(preprocessed);
        
        Card? bestMatch = null;
        var bestDistance = int.MaxValue;
        
        foreach (var candidate in candidates)
        {
            if (candidate.Hash == null || candidate.Hash.Length != queryHash.Length)
                continue;
            
            var distance = PerceptualHash.HammingDistance(queryHash, candidate.Hash);
            
            if (distance < bestDistance)
            {
                bestDistance = distance;
                bestMatch = candidate;
            }
            
            if (distance == 0)
                break;
        }
        
        if (bestMatch == null || bestDistance > MatchThreshold)
            return null;
        
        return (bestMatch, bestDistance, 0);
    }
    
    /// <summary>
    /// Rotate an image by the specified degrees (90, 180, or 270).
    /// </summary>
    private static SKBitmap RotateImage(SKBitmap source, int degrees)
    {
        var (newWidth, newHeight) = degrees switch
        {
            90 or 270 => (source.Height, source.Width),
            _ => (source.Width, source.Height)
        };
        
        var rotated = new SKBitmap(newWidth, newHeight, source.ColorType, source.AlphaType);
        
        using var canvas = new SKCanvas(rotated);
        canvas.Clear(SKColors.Black);
        
        canvas.Translate(newWidth / 2f, newHeight / 2f);
        canvas.RotateDegrees(degrees);
        canvas.Translate(-source.Width / 2f, -source.Height / 2f);
        canvas.DrawBitmap(source, 0, 0);
        
        return rotated;
    }
    
    public void Dispose()
    {
        _cacheLock.Dispose();
    }
}
