import ExpoModulesCore
import ImageIO
import Photos
import UIKit
import UniformTypeIdentifiers

public class ImageOrientationNormalizerModule: Module {
	public func definition() -> ModuleDefinition {
		Name("ImageOrientationNormalizer")

		AsyncFunction("normalizePickedImage") { (options: NormalizePickedImageOptions) -> NormalizedImage in
			return try self.normalize(options)
		}
	}

	private func normalize(_ options: NormalizePickedImageOptions) throws -> NormalizedImage {
		try validate(options)
		let quality = clampedQuality(options.quality)
		let maxLongEdge = options.maxLongEdge

		if let assetId = options.assetId, !assetId.isEmpty {
			if let result = try? renderViaPhotoKit(assetId: assetId, maxLongEdge: maxLongEdge, quality: quality) {
				return result
			}
			// PhotoKit miss (asset not found, permission denied, network unavailable) → fall through.
		}

		return try renderViaImageIO(uri: options.uri, maxLongEdge: maxLongEdge, quality: quality)
	}

	// MARK: - ImageIO path (metadata-faithful)

	private func renderViaImageIO(uri: String, maxLongEdge: Double, quality: Double) throws -> NormalizedImage {
		guard let url = parseURL(uri) else {
			throw NormalizationError.invalidUri(uri)
		}
		guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
			throw NormalizationError.cannotOpen(uri)
		}

		// kCGImageSourceCreateThumbnailWithTransform: true applies EXIF during decode.
		// Metadata-faithful — does NOT detect stale tags.
		let opts: [CFString: Any] = [
			kCGImageSourceCreateThumbnailFromImageAlways: true,
			kCGImageSourceCreateThumbnailWithTransform: true,
			kCGImageSourceThumbnailMaxPixelSize: NSNumber(value: maxLongEdge),
		]
		guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, opts as CFDictionary) else {
			throw NormalizationError.decodeFailed
		}

		return try writeJpeg(cgImage: cgImage, quality: quality, source: "imageio")
	}

	// MARK: - PhotoKit path (display-source — stale-EXIF-resistant)

	private func renderViaPhotoKit(assetId: String, maxLongEdge: Double, quality: Double) throws -> NormalizedImage {
		let fetch = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil)
		guard let asset = fetch.firstObject else {
			throw NormalizationError.assetNotFound(assetId)
		}

		let requestOpts = PHImageRequestOptions()
		requestOpts.isSynchronous = true
		requestOpts.deliveryMode = .highQualityFormat
		requestOpts.resizeMode = .exact
		requestOpts.isNetworkAccessAllowed = true

		var resultImage: UIImage?
		PHImageManager.default().requestImage(
			for: asset,
			targetSize: CGSize(width: maxLongEdge, height: maxLongEdge),
			contentMode: .aspectFit,
			options: requestOpts
		) { image, _ in
			resultImage = image
		}
		guard let uiImage = resultImage else {
			throw NormalizationError.photoKitFailed
		}

		let baked = try bakeOrientation(uiImage)
		return try writeJpeg(cgImage: baked, quality: quality, source: "photo-asset")
	}

	// MARK: - Helpers

	private func bakeOrientation(_ uiImage: UIImage) throws -> CGImage {
		// UIGraphicsImageRenderer.draw applies imageOrientation, producing display-space
		// pixels with .up orientation. format.scale = 1 keeps pixel dims == point dims.
		if uiImage.imageOrientation == .up, uiImage.scale == 1, let cg = uiImage.cgImage {
			return cg
		}
		let format = UIGraphicsImageRendererFormat()
		format.scale = 1
		let renderer = UIGraphicsImageRenderer(size: uiImage.size, format: format)
		let rendered = renderer.image { _ in
			uiImage.draw(at: .zero)
		}
		guard let cg = rendered.cgImage else {
			throw NormalizationError.decodeFailed
		}
		return cg
	}

	private func writeJpeg(cgImage: CGImage, quality: Double, source: String) throws -> NormalizedImage {
		let outputURL = cacheJpegURL()
		guard let dest = CGImageDestinationCreateWithURL(
			outputURL as CFURL,
			UTType.jpeg.identifier as CFString,
			1,
			nil
		) else {
			throw NormalizationError.destinationFailed
		}

		let writeOpts: [CFString: Any] = [
			kCGImageDestinationLossyCompressionQuality: quality,
			kCGImagePropertyOrientation: 1,
		]
		CGImageDestinationAddImage(dest, cgImage, writeOpts as CFDictionary)

		guard CGImageDestinationFinalize(dest) else {
			throw NormalizationError.encodeFailed
		}

		let result = NormalizedImage()
		result.uri = outputURL.absoluteString
		result.width = Double(cgImage.width)
		result.height = Double(cgImage.height)
		result.orientation = 1
		result.source = source
		return result
	}

	private func parseURL(_ uri: String) -> URL? {
		if let u = URL(string: uri), u.scheme != nil { return u }
		// Tolerate raw paths.
		return URL(fileURLWithPath: uri)
	}

	private func cacheJpegURL() -> URL {
		let dir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
		return dir.appendingPathComponent("normalized_\(UUID().uuidString).jpg")
	}

	private func validate(_ options: NormalizePickedImageOptions) throws {
		if options.uri.isEmpty {
			throw NormalizationError.invalidOptions("uri is required")
		}
		if options.maxLongEdge <= 0 {
			throw NormalizationError.invalidOptions("maxLongEdge must be > 0")
		}
	}

	private func clampedQuality(_ q: Double) -> Double {
		return max(0, min(1, q))
	}
}

struct NormalizePickedImageOptions: Record {
	@Field var uri: String = ""
	@Field var assetId: String?
	@Field var mimeType: String?
	@Field var maxLongEdge: Double = 2000
	@Field var quality: Double = 0.9
}

class NormalizedImage: Record {
	@Field var uri: String = ""
	@Field var width: Double = 0
	@Field var height: Double = 0
	@Field var orientation: Int = 1
	@Field var source: String = "fallback"

	required init() {}
}

enum NormalizationError: Error, LocalizedError {
	case invalidUri(String)
	case invalidOptions(String)
	case cannotOpen(String)
	case decodeFailed
	case destinationFailed
	case encodeFailed
	case assetNotFound(String)
	case photoKitFailed

	var errorDescription: String? {
		switch self {
		case .invalidUri(let uri): return "Invalid URI: \(uri)"
		case .invalidOptions(let reason): return "Invalid options: \(reason)"
		case .cannotOpen(let uri): return "Cannot open image source: \(uri)"
		case .decodeFailed: return "Failed to decode source image"
		case .destinationFailed: return "Failed to create JPEG destination"
		case .encodeFailed: return "Failed to encode JPEG output"
		case .assetNotFound(let id): return "PhotoKit asset not found: \(id)"
		case .photoKitFailed: return "PhotoKit returned no image"
		}
	}
}
