//
//  EditorViewModel.swift
//  CheapRetouch
//
//  View model coordinating edit operations between services and UI.
//

import SwiftUI
import UIKit
import CoreGraphics
import Observation

// MARK: - Accessibility Helpers

private func announceForVoiceOver(_ message: String) {
    UIAccessibility.post(notification: .announcement, argument: message)
}

@Observable
@MainActor
final class EditorViewModel {

    // MARK: - State

    var originalImage: CGImage?
    var editedImage: CGImage?
    var maskPreview: CGImage?
    var selectedTool: EditTool = .person
    var brushSize: CGFloat = 20
    var featherAmount: CGFloat = 4
    var wireWidth: CGFloat = 6

    var isProcessing = false
    var processingMessage = ""
    var errorMessage: String?
    var showingMaskConfirmation = false
    var detectedPeopleCount = 0
    var showSelectAllPeople = false
    var isLineBrushMode = false
    var lineBrushPath: [CGPoint] = []
    var useHighContrastMask = false
    var useEdgeRefinement = true
    var pendingRefineMask: CGImage?
    var isLowConfidenceMask = false

    private(set) var project: Project?

    // MARK: - Services

    private let maskingService = MaskingService()
    private let contourService = ContourService()
    private let inpaintEngine = InpaintEngine()
    private let imagePipeline = ImagePipeline()

    // MARK: - Computed Properties

    var displayImage: CGImage? {
        editedImage ?? originalImage
    }

    var canUndo: Bool {
        project?.canUndo ?? false
    }

    var canRedo: Bool {
        project?.canRedo ?? false
    }

    var imageSize: CGSize {
        guard let image = originalImage else { return .zero }
        return CGSize(width: image.width, height: image.height)
    }

    // MARK: - Image Loading

    func loadImage(_ uiImage: UIImage, localIdentifier: String? = nil) {
        DebugLogger.action("loadImage called")
        
        // Normalize image orientation - fixes photos that appear rotated
        let normalizedImage = normalizeImageOrientation(uiImage)
        
        guard let cgImage = normalizedImage.cgImage else {
            DebugLogger.error("Failed to get CGImage from UIImage")
            return
        }
        DebugLogger.imageInfo("Loading image", image: cgImage)

        originalImage = cgImage
        editedImage = nil
        maskPreview = nil
        errorMessage = nil
        pendingRefineMask = nil
        showSelectAllPeople = false
        detectedPeopleCount = 0

        // Check image size and warn if large
        let pixelCount = cgImage.width * cgImage.height
        if pixelCount > 48_000_000 {
            errorMessage = "Very large image (48MP+). May cause memory issues."
        } else if pixelCount > 12_000_000 {
            // Just a note, not an error - processing continues
        }

        // Create new project with appropriate image source
        let imageSource: Project.ImageSource
        if let identifier = localIdentifier {
            imageSource = .photoLibrary(localIdentifier: identifier)
        } else {
            let imageData = normalizedImage.jpegData(compressionQuality: 0.9) ?? Data()
            imageSource = .embedded(data: imageData)
        }
        project = Project(imageSource: imageSource)

        announceForVoiceOver("Photo loaded")
    }
    
    /// Normalizes image orientation by redrawing with correct transform
    private func normalizeImageOrientation(_ image: UIImage) -> UIImage {
        // If orientation is already up, no need to redraw
        guard image.imageOrientation != .up else { return image }
        
        // Redraw image with correct orientation applied
        UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
        image.draw(in: CGRect(origin: .zero, size: image.size))
        let normalizedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        return normalizedImage ?? image
    }

    // MARK: - Tap Handling

    func handleTap(at point: CGPoint) async {
        DebugLogger.action("handleTap at point: \(point), tool: \(selectedTool)")
        guard let image = displayImage else {
            DebugLogger.error("handleTap: No display image available")
            return
        }
        DebugLogger.imageInfo("Current display image", image: image)

        isProcessing = true
        errorMessage = nil

        do {
            switch selectedTool {
            case .person:
                processingMessage = "Detecting person..."
                DebugLogger.processing("Starting person detection")
                try await handlePersonTap(at: point, in: image)

            case .object:
                processingMessage = "Detecting object..."
                DebugLogger.processing("Starting object detection")
                try await handleObjectTap(at: point, in: image)

            case .wire:
                processingMessage = "Detecting wire..."
                DebugLogger.processing("Starting wire detection")
                try await handleWireTap(at: point, in: image)

            case .brush:
                DebugLogger.log("Brush tool - tap ignored (handled by canvas)")
                break
            }
        } catch {
            DebugLogger.error("handleTap failed", error: error)
            errorMessage = error.localizedDescription
        }

        isProcessing = false
        processingMessage = ""
    }

    private func handlePersonTap(at point: CGPoint, in image: CGImage) async throws {
        DebugLogger.log("handlePersonTap at \(point)")
        let result = try await maskingService.generatePersonMaskWithCount(at: point, in: image)

        detectedPeopleCount = result.instanceCount
        DebugLogger.state("Detected \(result.instanceCount) people, confidence: \(result.confidence)")

        guard let mask = result.mask else {
            DebugLogger.log("No person found at tap location")
            errorMessage = "No person found at tap location"
            return
        }

        DebugLogger.imageInfo("Person mask before dilation", image: mask)

        // Dilate mask by 3px to capture edge pixels
        let dilatedMask = dilateMask(mask, by: 3)
        DebugLogger.imageInfo("Person mask after dilation", image: dilatedMask)

        maskPreview = dilatedMask ?? mask
        showingMaskConfirmation = true
        showSelectAllPeople = result.instanceCount > 1

        // Check for low confidence and flag for user warning
        isLowConfidenceMask = result.confidence < 0.7
        DebugLogger.state("Mask preview set, lowConfidence: \(isLowConfidenceMask)")
    }

    func selectAllPeople() async {
        guard let image = displayImage else { return }

        isProcessing = true
        processingMessage = "Selecting all people..."

        do {
            let mask = try await maskingService.generateAllForegroundMasks(in: image)

            guard let mask = mask else {
                errorMessage = "No people found in image"
                isProcessing = false
                processingMessage = ""
                return
            }

            // Dilate mask
            let dilatedMask = dilateMask(mask, by: 3)
            maskPreview = dilatedMask ?? mask
            showingMaskConfirmation = true
            showSelectAllPeople = false
        } catch {
            errorMessage = error.localizedDescription
        }

        isProcessing = false
        processingMessage = ""
    }

    private func dilateMask(_ mask: CGImage, by pixels: Int) -> CGImage? {
        let width = mask.width
        let height = mask.height

        guard let context = CGContext(
            data: nil,
            width: width,
            height: height,
            bitsPerComponent: 8,
            bytesPerRow: width,
            space: CGColorSpaceCreateDeviceGray(),
            bitmapInfo: CGImageAlphaInfo.none.rawValue
        ) else { return nil }

        // Draw original mask
        context.draw(mask, in: CGRect(x: 0, y: 0, width: width, height: height))

        // Apply dilation by drawing the mask multiple times with offsets
        context.setBlendMode(.lighten)
        for dx in -pixels...pixels {
            for dy in -pixels...pixels {
                if dx * dx + dy * dy <= pixels * pixels {
                    context.draw(mask, in: CGRect(x: dx, y: dy, width: width, height: height))
                }
            }
        }

        return context.makeImage()
    }

    private func handleObjectTap(at point: CGPoint, in image: CGImage) async throws {
        DebugLogger.log("handleObjectTap at \(point)")
        DebugLogger.log("Calling maskingService.generateForegroundMask...")

        let mask = try await maskingService.generateForegroundMask(at: point, in: image)

        DebugLogger.log("generateForegroundMask returned")
        DebugLogger.imageInfo("Object mask", image: mask)

        guard let mask = mask else {
            DebugLogger.log("No mask returned - prompting for brush tool")
            errorMessage = "Couldn't detect object. Try the brush tool to select manually."
            return
        }

        maskPreview = mask
        showingMaskConfirmation = true
        DebugLogger.state("Object mask preview set, showing confirmation")
    }

    private func handleWireTap(at point: CGPoint, in image: CGImage) async throws {
        // If in line brush mode, don't process taps
        if isLineBrushMode {
            return
        }

        let contours = try await contourService.detectContours(in: image)
        let bestContour = await contourService.findBestWireContour(
            at: point,
            from: contours,
            imageSize: CGSize(width: image.width, height: image.height)
        )

        guard let contour = bestContour else {
            errorMessage = "No lines detected. Tap 'Line Brush' to draw along the wire."
            return
        }

        let mask = await contourService.contourToMask(
            contour,
            width: Int(wireWidth),
            imageSize: CGSize(width: image.width, height: image.height)
        )

        guard let mask = mask else {
            errorMessage = "Failed to create mask from contour"
            return
        }

        maskPreview = mask
        showingMaskConfirmation = true
    }

    // MARK: - Line Brush Mode

    func toggleLineBrushMode() {
        isLineBrushMode.toggle()
        if !isLineBrushMode {
            lineBrushPath.removeAll()
        }
    }

    func addLineBrushPoint(_ point: CGPoint) {
        lineBrushPath.append(point)
    }

    func finishLineBrush() async {
        DebugLogger.action("finishLineBrush called, path points: \(lineBrushPath.count)")
        guard let image = displayImage, lineBrushPath.count >= 2 else {
            DebugLogger.log("Not enough points or no image, clearing path")
            lineBrushPath.removeAll()
            return
        }

        DebugLogger.imageInfo("Source image for line brush", image: image)

        isProcessing = true
        processingMessage = "Creating line mask..."

        // Generate mask from line brush path
        let mask = createLineBrushMask(
            path: lineBrushPath,
            width: Int(wireWidth),
            imageSize: CGSize(width: image.width, height: image.height)
        )

        lineBrushPath.removeAll()

        guard let mask = mask else {
            DebugLogger.error("Failed to create mask from line brush")
            errorMessage = "Failed to create mask from line brush"
            isProcessing = false
            processingMessage = ""
            return
        }

        DebugLogger.imageInfo("Line brush mask created", image: mask)
        maskPreview = mask
        showingMaskConfirmation = true
        isLineBrushMode = false
        isProcessing = false
        processingMessage = ""
    }

    private func createLineBrushMask(path: [CGPoint], width: Int, imageSize: CGSize) -> CGImage? {
        let intWidth = Int(imageSize.width)
        let intHeight = Int(imageSize.height)

        guard let context = CGContext(
            data: nil,
            width: intWidth,
            height: intHeight,
            bitsPerComponent: 8,
            bytesPerRow: intWidth,
            space: CGColorSpaceCreateDeviceGray(),
            bitmapInfo: CGImageAlphaInfo.none.rawValue
        ) else { return nil }

        context.setFillColor(gray: 0, alpha: 1)
        context.fill(CGRect(x: 0, y: 0, width: intWidth, height: intHeight))

        context.setStrokeColor(gray: 1, alpha: 1)
        context.setLineWidth(CGFloat(width))
        context.setLineCap(.round)
        context.setLineJoin(.round)

        // Use Catmull-Rom spline for smooth path
        if path.count >= 4 {
            let smoothedPath = catmullRomSpline(points: path, segments: 10)
            context.move(to: CGPoint(x: smoothedPath[0].x, y: imageSize.height - smoothedPath[0].y))
            for point in smoothedPath.dropFirst() {
                context.addLine(to: CGPoint(x: point.x, y: imageSize.height - point.y))
            }
        } else {
            context.move(to: CGPoint(x: path[0].x, y: imageSize.height - path[0].y))
            for point in path.dropFirst() {
                context.addLine(to: CGPoint(x: point.x, y: imageSize.height - point.y))
            }
        }

        context.strokePath()
        return context.makeImage()
    }

    private func catmullRomSpline(points: [CGPoint], segments: Int) -> [CGPoint] {
        guard points.count >= 4 else { return points }

        var result: [CGPoint] = []

        for i in 0..<(points.count - 1) {
            let p0 = points[max(0, i - 1)]
            let p1 = points[i]
            let p2 = points[min(points.count - 1, i + 1)]
            let p3 = points[min(points.count - 1, i + 2)]

            for t in 0..<segments {
                let t0 = CGFloat(t) / CGFloat(segments)
                let t2 = t0 * t0
                let t3 = t2 * t0

                let x = 0.5 * ((2 * p1.x) +
                              (-p0.x + p2.x) * t0 +
                              (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
                              (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3)
                let y = 0.5 * ((2 * p1.y) +
                              (-p0.y + p2.y) * t0 +
                              (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
                              (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3)

                result.append(CGPoint(x: x, y: y))
            }
        }

        result.append(points.last!)
        return result
    }

    // MARK: - Mask Confirmation

    func confirmMask() async {
        DebugLogger.action("confirmMask called")
        guard let image = displayImage, let mask = maskPreview else {
            DebugLogger.error("confirmMask: Missing image or mask")
            return
        }

        DebugLogger.imageInfo("Source image for inpaint", image: image)
        DebugLogger.imageInfo("Mask for inpaint", image: mask)

        isProcessing = true
        processingMessage = "Removing..."

        do {
            DebugLogger.processing("Starting inpaint...")
            let result = try await inpaintEngine.inpaint(image: image, mask: mask)
            DebugLogger.imageInfo("Inpaint result", image: result)
            editedImage = result

            // Add operation to project
            if var project = project {
                if let maskDataObj = MaskData(from: mask) {
                    let maskOp = MaskOperation(
                        toolType: toolTypeForCurrentTool(),
                        maskData: maskDataObj.data,
                        maskWidth: maskDataObj.width,
                        maskHeight: maskDataObj.height
                    )
                    let inpaintOp = InpaintOperation(maskOperationId: maskOp.id, featherAmount: Float(featherAmount))

                    project.addOperation(.mask(maskOp))
                    project.addOperation(.inpaint(inpaintOp))
                    self.project = project
                }
            }

            announceForVoiceOver("Removal complete")
        } catch {
            errorMessage = error.localizedDescription
        }

        maskPreview = nil
        showingMaskConfirmation = false
        showSelectAllPeople = false
        isLowConfidenceMask = false
        isProcessing = false
        processingMessage = ""
    }

    func cancelMask() {
        maskPreview = nil
        showingMaskConfirmation = false
        showSelectAllPeople = false
        pendingRefineMask = nil
        isLowConfidenceMask = false
    }

    func refineWithBrush() {
        // Save current mask for refinement and switch to brush tool
        pendingRefineMask = maskPreview
        maskPreview = nil
        showingMaskConfirmation = false
        showSelectAllPeople = false
        selectedTool = .brush
        announceForVoiceOver("Switched to brush tool for mask refinement")
    }

    // MARK: - Undo/Redo

    func undo() async {
        guard var project = project, project.undo() else { return }
        self.project = project
        await rebuildEditedImage()
        announceForVoiceOver("Undo complete")
    }

    func redo() async {
        guard var project = project, project.redo() else { return }
        self.project = project
        await rebuildEditedImage()
        announceForVoiceOver("Redo complete")
    }

    private func rebuildEditedImage() async {
        guard let original = originalImage, let project = project else { return }

        isProcessing = true
        processingMessage = "Rebuilding..."

        do {
            let result = try await imagePipeline.renderPreview(
                originalImage: original,
                operations: project.activeOperations
            )
            editedImage = result
        } catch {
            errorMessage = error.localizedDescription
        }

        isProcessing = false
        processingMessage = ""
    }

    // MARK: - Brush Tool

    func applyBrushMask(_ mask: CGImage) async {
        DebugLogger.action("applyBrushMask called")
        DebugLogger.imageInfo("Brush mask", image: mask)
        maskPreview = mask
        await confirmMask()
    }

    // MARK: - Helpers

    private func toolTypeForCurrentTool() -> ToolType {
        switch selectedTool {
        case .person: return .person
        case .object: return .object
        case .wire: return .wire
        case .brush: return .brush
        }
    }
}
