import AVFoundation
import CoreMedia

protocol HooksCameraAssetWriterDelegate: AnyObject {
    func didStartWriting(_ to: URL?)
    func didUpdateWriting(_ formattedTime: String, _ completionRatio: CGFloat)
    func didEndWriting(_ to: URL?)
}

final class HooksCameraAssetWriter {
    weak var delegate: HooksCameraAssetWriterDelegate?

    private var assetWriter: AVAssetWriter?
    private var videoInput: AVAssetWriterInput?
    private var pixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?

    private var isRecording = false
    private var sessionStarted = false
    private var recordingStartTime: CMTime?
    private var startDateTime = Date.now
    private var hasWarmedUp = false

    // MARK: – Public API ------------------------------------------------------

    /// Convenience helper that picks a unique file name and records with no extra rotation.
    @discardableResult
    func startRecordingToUniqueFile() -> URL? {
        guard let url = Self.createUniqueFileURLInDocuments("mp4") else { return nil }
        startRecording(to: url, triggerDelegate: true)
        return url
    }

    /// Main entry point: supply rotation (°) & mirroring from the AVCaptureConnection.
    func startRecording(
        to url: URL,
        triggerDelegate: Bool
    ) {
        do {
            let writer = try AVAssetWriter(outputURL: url, fileType: .mp4)
            assetWriter = writer

            // TODO: Document this
            let sensorWidth: Int = 1920
            let sensorHeight: Int = 1080

            let outputSettings: [String: Any] = [
                AVVideoCodecKey: AVVideoCodecType.h264,
                AVVideoWidthKey: sensorWidth, // 1920
                AVVideoHeightKey: sensorHeight, // 1080
            ]

            let input = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
            input.expectsMediaDataInRealTime = true

            guard writer.canAdd(input) else {
                log.error("[ERROR] Cannot add video input")
                return
            }
            writer.add(input)

            let adaptor = AVAssetWriterInputPixelBufferAdaptor(
                assetWriterInput: input,
                sourcePixelBufferAttributes: [
                    kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
                    kCVPixelBufferWidthKey as String: sensorWidth, // 1920
                    kCVPixelBufferHeightKey as String: sensorHeight, // 1080
                ]
            )

            self.videoInput = input
            self.pixelBufferAdaptor = adaptor
            self.sessionStarted = false

            writer.startWriting()

            if triggerDelegate {
                delegate?.didStartWriting(url)
            }

            recordingStartTime = nil
            isRecording = true
            startDateTime = Date.now

        } catch {
            log.error("[ERROR] Failed to start writer: \(error)")
        }
    }

    // MARK: – Warm‑up ---------------------------------------------------------

    /// Call this once during app launch to prime the encoder and remove the first‑record lag.
    /// Supply the same orientation parameters you will use for real recording.
    @MainActor
    func warmUpEncoder(videoRotationAngle _: CGFloat = 0, mirrored _: Bool = false) async {
        log.info("[HOOKS] Warming up video asset writer (*Intentional)")
        guard !hasWarmedUp, !isRecording else { return }
        guard let tmpURL = Self.createUniqueFileURLInDocuments("mp4") else { return }

        // Start a tiny recording
        startRecording(
            to: tmpURL,
            triggerDelegate: false
        )

        // Ensure writer exists
        guard
            let writer = assetWriter,
            let adaptor = pixelBufferAdaptor
        else { return }

        // Manually start the session at t=0 since we don't have a CMSampleBuffer
        if !sessionStarted {
            writer.startSession(atSourceTime: .zero)
            sessionStarted = true
            recordingStartTime = .zero
        }

        // Append a single black frame
        if let blank = Self.makeBlankPixelBuffer(width: 1080, height: 1920) {
            adaptor.append(blank, withPresentationTime: .zero)
        }

        // Finish and delete the warm‑up file
        _ = await stopRecording(triggerDelegate: false)
        try? FileManager.default.removeItem(at: tmpURL)

        hasWarmedUp = true
    }

    // MARK: – Stop ------------------------------------------------------------

    @discardableResult
    func stopRecording(triggerDelegate: Bool = true) async -> URL? {
        guard isRecording, let writer = assetWriter else { return nil }

        isRecording = false
        videoInput?.markAsFinished()

        await writer.finishWriting()

        let outputURL: URL?
        if writer.status == .completed {
            outputURL = writer.outputURL
        } else {
            log.error("[ERROR] Writer failed: \(writer.error?.localizedDescription ?? "Unknown error")")
            outputURL = nil
        }

        if triggerDelegate {
            DispatchQueue.main.async { self.delegate?.didEndWriting(outputURL) }
        }

        assetWriter = nil
        videoInput = nil
        pixelBufferAdaptor = nil
        recordingStartTime = nil
        sessionStarted = false

        return outputURL
    }

    // MARK: – Frame ingest ----------------------------------------------------

    func writeSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
        guard
            isRecording,
            let pixelBuffer = sampleBuffer.imageBuffer,
            let writer = assetWriter,
            let input = videoInput,
            let adaptor = pixelBufferAdaptor
        else { return }

        let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
        let elapsedTime = Date.now.timeIntervalSince(startDateTime)
        let (formatted, completion) = Self.formatTime(elapsedTime)
        delegate?.didUpdateWriting(formatted, completion)

        if !sessionStarted {
            writer.startSession(atSourceTime: timestamp)
            recordingStartTime = timestamp
            sessionStarted = true
        }

        if input.isReadyForMoreMediaData {
            adaptor.append(pixelBuffer, withPresentationTime: timestamp)
        }
    }
}

/* Temporary File Related */

extension HooksCameraAssetWriter {
    static var temporaryFilePrefix: String {
        return "hooks-temp-file-"
    }

    static func createUniqueFileURLInDocuments(_ ext: String) -> URL? {
        guard let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            log.error("Failed to locate Documents directory.")
            return nil
        }
        let filename = temporaryFilePrefix + UUID().uuidString + "." + ext
        return docs.appendingPathComponent(filename)
    }

    static func deleteTemporaryFiles() {
        let fileManager = FileManager.default

        guard
            let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
            let files = try? fileManager.contentsOfDirectory(atPath: documentsDirectory.path)
        else { return }

        for file in files {
            // Check if the file starts with the given prefix
            if file.hasPrefix(temporaryFilePrefix) {
                let filePath = documentsDirectory.appendingPathComponent(file).path
                do {
                    try fileManager.removeItem(atPath: filePath)
                } catch {
                    log.error("Hooks Camera: Couldn't delete temporary file: \(filePath)")
                }
            }
        }
    }
}

// MARK: – Helpers -------------------------------------------------------------

extension HooksCameraAssetWriter {
    enum Constants {
        static let maxRecordingLimit: TimeInterval = 120.0
        static let minRecordingLimit: TimeInterval = 10.0
    }

    static func formatTime(_ elapsed: TimeInterval) -> (String, CGFloat) {
        let formatted = formatTimeIntervalAsSeconds(elapsed)
        let ratio = min(elapsed / Constants.maxRecordingLimit, 1.0)
        return (formatted, CGFloat(ratio))
    }

    static func formatTimeIntervalAsSeconds(_ t: TimeInterval) -> String {
        return t.toMinuteSecondString()
    }

    /// Builds an all‑black `CVPixelBuffer`.
    static func makeBlankPixelBuffer(width: Int, height: Int) -> CVPixelBuffer? {
        var px: CVPixelBuffer?
        let attrs: [String: Any] = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA,
            kCVPixelBufferCGImageCompatibilityKey as String: true,
            kCVPixelBufferCGBitmapContextCompatibilityKey as String: true,
        ]
        guard CVPixelBufferCreate(kCFAllocatorDefault, width, height, kCVPixelFormatType_32BGRA, attrs as CFDictionary, &px) == kCVReturnSuccess,
              let buffer = px
        else { return nil }

        CVPixelBufferLockBaseAddress(buffer, [])
        if let base = CVPixelBufferGetBaseAddress(buffer) {
            memset(base, 0, CVPixelBufferGetDataSize(buffer))
        }
        CVPixelBufferUnlockBaseAddress(buffer, [])
        return buffer
    }
}
