import APIClient
import AudioRecorderClient
import AVFoundation
import ComposableArchitecture
import Errors
import Foundation
import SunoModelClient
import UIKit
import Utilities
import Localization

@Reducer
public struct ChatAudioRecorder {
    @Reducer(state: .equatable)
    public enum Destination {
        case minLengthAlert(AlertState<Alert>)
        case maxTimeReachedAlert(AlertState<Alert>)

        public enum Alert {
            case ok
        }
    }

    @ObservableState
    public struct State: Equatable, Sendable {
        @Presents public var destination: Destination.State?

        static let minDuration: TimeInterval = 5.0

        var date: Date
        var duration: TimeInterval = 0
        var url: URL
        var samples: [TimeInterval: CGFloat] = [:]
        var startDate: Date?
        var showProgressView: Bool = false

        var hasStartedRecording: Bool = false

        var isRecording: Bool {
            hasStartedRecording && startDate != nil
        }

        // Trimming state
        var isTrimming: Bool = false
        var trimStartTime: TimeInterval = 0
        var trimEndTime: TimeInterval = 0
        var isPlaying: Bool = false
        var playbackTime: TimeInterval = 0
        var title: String = "Untitled"
        var trimmedUrl: URL? // Track trimmed file for cleanup on dismissal

        // Drag state for trimmer
        var dragOriginalStartTime: TimeInterval = 0
        var dragOriginalEndTime: TimeInterval = 0
        var isDraggingStartHandle: Bool = false
        var isDraggingEndHandle: Bool = false
        var isDraggingSelection: Bool = false

        var maxRecordTime: TimeInterval {
            @Shared(.inMemory(.billingInfo)) var billingInfo: SubscriptionInfoResponse?
            if let billingInfo {
                return TimeInterval(billingInfo.audioUploadLimits.max)
            } else {
                return 60 // Fallback to 60
            }
        }

        var titleCharCountLimit: Int {
            @Shared(.inMemory(.selectedSunoModel)) var selectedSunoModel: SunoModelMetaData = .modelDefault
            let defaultCharCountLimit: Int = 200
            return selectedSunoModel.maxLengths?.title ?? defaultCharCountLimit
        }
    }

    public enum Action: BindableAction {
        @CasePathable
        public enum Delegate: Sendable, Equatable {
            case didFinish(Result<AudioRecording, AnyError>)
        }

        case binding(BindingAction<State>)
        case delegate(Delegate)
        case destination(PresentationAction<Destination.Action>)

        case task
        case startRecordingTapped
        case stopButtonTapped
        case audioRecorderDidFinish(Result<Bool, AnyError>)
        case finalRecordingTime(TimeInterval)
        case timerUpdated
        case updateSamples(PowerLevels)

        // Trimming actions
        case setupAudioPlayer
        case playPreview
        case pausePreview
        case playbackTimeUpdated(TimeInterval)
        case teardownAudioPlayer
        case clearUntitledTitle
        // Drag actions
        case dragStartHandleStarted
        case dragStartHandleChanged(translation: CGFloat, totalWidth: CGFloat)
        case dragStartHandleEnded
        case dragEndHandleStarted
        case dragEndHandleChanged(translation: CGFloat, totalWidth: CGFloat)
        case dragEndHandleEnded
        case dragSelectionStarted
        case dragSelectionChanged(translation: CGFloat, totalWidth: CGFloat)
        case dragSelectionEnded
        case confirmTrim
        case cancelTrim
        case trimAudioCompleted(Result<URL, AnyError>)
    }

    @Dependency(\.audioRecorder) private var audioRecorder
    @Dependency(\.continuousClock) private var clock
    @Dependency(\.date) private var date
    @Dependency(\.temporaryDirectory) private var temporaryDirectory
    @Dependency(\.uuid) private var uuid

    private let log = Logger(category: "ChatAudioRecorder")

    public init() {}

    private func trimAudio(
        inputUrl: URL,
        startTime: TimeInterval,
        endTime: TimeInterval,
        title: String,
        titleCharCountLimit: Int
    ) async throws -> URL {
        let asset = AVURLAsset(url: inputUrl)
        let startCMTime = CMTime(seconds: startTime, preferredTimescale: 600)
        let endCMTime = CMTime(seconds: endTime, preferredTimescale: 600)
        let duration = CMTimeSubtract(endCMTime, startCMTime)
        let timeRange = CMTimeRange(start: startCMTime, duration: duration)

        let composition = AVMutableComposition()

        guard let audioTrack = try await asset.loadTracks(withMediaType: .audio).first else {
            throw AudioTrimmingError.audioTrackMissing
        }

        guard let compositionTrack = composition.addMutableTrack(
            withMediaType: .audio,
            preferredTrackID: kCMPersistentTrackID_Invalid
        ) else {
            throw AudioTrimmingError.compositionFailed
        }

        try compositionTrack.insertTimeRange(timeRange, of: audioTrack, at: .zero)

        let sanitizedTitle = sanitizeFilename(title, maxLength: titleCharCountLimit)
        let uniqueFilename = "\(sanitizedTitle)-\(uuid().uuidString)"
        let outputUrl = temporaryDirectory()
            .appendingPathComponent(uniqueFilename)
            .appendingPathExtension("m4a")

        guard let exportSession = AVAssetExportSession(
            asset: composition,
            presetName: AVAssetExportPresetAppleM4A
        ) else {
            throw AudioTrimmingError.exportSessionFailed
        }

        exportSession.timeRange = CMTimeRange(start: .zero, duration: duration)
        exportSession.shouldOptimizeForNetworkUse = true

        do {
            var taskID: UIBackgroundTaskIdentifier = .invalid
            await MainActor.run {
                taskID = UIApplication.shared.beginBackgroundTask {
                    exportSession.cancelExport()
                }
            }
            defer {
                Task { @MainActor in
                    if taskID != .invalid {
                        UIApplication.shared.endBackgroundTask(taskID)
                    }
                }
            }
            try await exportSession.export(to: outputUrl, as: .m4a)
        } catch {
            throw AudioTrimmingError.exportFailed(error.localizedDescription)
        }

        return outputUrl
    }

    private func handlePlayback(
        url: URL,
        trimStartTime: TimeInterval,
        trimEndTime: TimeInterval,
        send: Send<Action>
    ) async {
        guard trimStartTime >= 0, trimEndTime > trimStartTime else {
            let error = AudioTrimmingError.invalidTrimTimes(
                startTime: trimStartTime,
                endTime: trimEndTime,
                duration: 0,
                trimmedDuration: trimEndTime - trimStartTime
            )
            log.telemetry.error(error, message: "Invalid trim times for playback")
            await send(.pausePreview)
            return
        }

        let player: AVAudioPlayer?
        do {
            player = try await MainActor.run {
                let player = try AVAudioPlayer(contentsOf: url)
                player.prepareToPlay()
                return player
            }
        } catch {
            log.telemetry.error(error, message: "Failed to create audio player for playback")
            await send(.pausePreview)
            return
        }
        guard let player = player else { return }

        let startTime = max(trimStartTime, 0)
        await MainActor.run {
            player.currentTime = startTime
            player.play()
        }

        defer {
            Task { @MainActor in
                player.stop()
            }
        }

        for await _ in clock.timer(interval: .milliseconds(100)) {
            let (currentTime, isStillPlaying) = await MainActor.run { () -> (TimeInterval, Bool) in
                (player.currentTime, player.isPlaying)
            }

            // Check if playback reached the end of the trim range or stopped
            if currentTime >= trimEndTime || !isStillPlaying {
                // Stop playback and reset to start
                await MainActor.run {
                    player.pause()
                    player.currentTime = startTime
                }
                await send(.pausePreview)
                await send(.playbackTimeUpdated(startTime))
                break
            } else if currentTime < trimStartTime {
                // If playback somehow goes before start, seek to start
                await MainActor.run {
                    player.currentTime = startTime
                }
                await send(.playbackTimeUpdated(startTime))
            } else {
                await send(.playbackTimeUpdated(currentTime))
            }
        }
    }

    /// Resets all recording state to initial values for re-recording.
    /// Returns the old URL and trimmed URL (if any) for cleanup.
    private func resetRecordingState(_ state: inout State) -> (oldUrl: URL, trimmedUrl: URL?) {
        state.isTrimming = false
        state.hasStartedRecording = false
        state.duration = 0
        state.samples.removeAll()
        state.startDate = nil
        state.trimStartTime = 0
        state.trimEndTime = 0
        state.isPlaying = false
        state.playbackTime = 0
        state.title = "Untitled"
        state.date = date.now
        let oldUrl = state.url
        state.url = temporaryDirectory()
            .appendingPathComponent(uuid().uuidString)
            .appendingPathExtension("m4a")
        state.isDraggingStartHandle = false
        state.isDraggingEndHandle = false
        state.isDraggingSelection = false
        state.dragOriginalStartTime = 0
        state.dragOriginalEndTime = 0
        let trimmedUrl = state.trimmedUrl
        state.trimmedUrl = nil
        return (oldUrl, trimmedUrl)
    }

    /// Returns an effect that cleans up the specified files with proper error logging
    private func cleanupFiles(_ urls: [URL], context: String) -> Effect<Action> {
        .run { [log = self.log] _ in
            for url in urls {
                do {
                    try FileManager.default.removeItem(at: url)
                } catch {
                    log.telemetry.error(error, message: "Failed to delete file in \(context): \(url.lastPathComponent)")
                }
            }
        }
    }

    private func sanitizeFilename(_ title: String, maxLength: Int) -> String {
        // Sanitize title for use as filename:
        // - Lowercase
        // - Replace spaces with dashes
        // - Remove invalid filename characters
        // - Limit length to respect both model limit and filesystem limit
        let invalidCharacters = CharacterSet(charactersIn: "/:?<>\\|*\"")
        let sanitized = title
            .lowercased()
            .replacingOccurrences(of: " ", with: "-")
            .components(separatedBy: invalidCharacters)
            .joined(separator: "")
            .trimmingCharacters(in: .whitespacesAndNewlines)

        if sanitized.isEmpty {
            return uuid().uuidString
        }

        let filesystemLimit = 255
        let effectiveLimit = min(maxLength, filesystemLimit)
        if sanitized.count > effectiveLimit {
            return String(sanitized.prefix(effectiveLimit))
        }

        return sanitized
    }

    public enum AudioRecorderError: LocalizedError, Equatable {
        case recordingFailed

        public var errorDescription: String? {
            switch self {
            case .recordingFailed:
                return "Recording failed"
            }
        }
    }

    public enum AudioTrimmingError: LocalizedError, Equatable {
        case audioTrackMissing
        case compositionFailed
        case exportSessionFailed
        case exportFailed(String)
        case invalidTrimTimes(startTime: TimeInterval, endTime: TimeInterval, duration: TimeInterval, trimmedDuration: TimeInterval)

        public var errorDescription: String? {
            switch self {
            case .audioTrackMissing:
                return "Audio track missing"
            case .compositionFailed:
                return "Failed to create composition"
            case .exportSessionFailed:
                return "Failed to create export session"
            case .exportFailed(let message):
                return "Export failed: \(message)"
            case .invalidTrimTimes(let startTime, let endTime, let duration, let trimmedDuration):
                return "Invalid trim times: start=\(startTime), end=\(endTime), duration=\(duration), trimmedDuration=\(trimmedDuration)"
            }
        }
    }

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce<State, Action> { state, action in
            struct ClockCancellable: Hashable {}
            struct PlaybackTimerCancellable: Hashable {}
            switch action {
            case .task:
                return .run { _ in
                    try? await audioRecorder.prepareToRecordWithEngine()
                }

            case .startRecordingTapped:
                guard !state.hasStartedRecording else { return .none }
                state.hasStartedRecording = true
                state.startDate = date.now
                state.samples.removeAll()
                return .merge(
                    .cancel(id: ClockCancellable()),
                    .run { [url = state.url] send in
                        Task {
                            do {
                                try await self.audioRecorder.startRecordingWithEngine(url: url, shouldStop: true)
                                guard !Task.isCancelled else { return }
                                await send(.audioRecorderDidFinish(.success(true)))
                            } catch {
                                guard !Task.isCancelled else { return }
                                log.telemetry.error(error, message: "Failed to start audio recording with engine")
                                await send(.audioRecorderDidFinish(.failure(AnyError(error))))
                            }
                        }

                        // Start timer for UI updates
                        for await _ in self.clock.timer(interval: .seconds(0.1)) {
                            await send(.timerUpdated)
                        }
                    }
                    .cancellable(id: ClockCancellable())
                )


            case .audioRecorderDidFinish(.success(true)):
                return .cancel(id: ClockCancellable())

            case .audioRecorderDidFinish(.success(false)):
                state.hasStartedRecording = false
                state.startDate = nil
                let recordingError = AudioRecorderError.recordingFailed
                log.telemetry.error(recordingError, message: "Audio recording failed - recorder returned false")
                return .merge(
                    .cancel(id: ClockCancellable()),
                    .send(.delegate(.didFinish(.failure(AnyError(recordingError)))))
                )

            case .audioRecorderDidFinish(.failure(let error)):
                state.hasStartedRecording = false
                state.startDate = nil
                log.telemetry.error(error, message: "Audio recording failed with error")
                return .merge(
                    .cancel(id: ClockCancellable()),
                    .send(.delegate(.didFinish(.failure(error))))
                )

            case .finalRecordingTime(let duration):
                state.duration = duration
                state.showProgressView = false
                state.isTrimming = true
                state.trimStartTime = 0
                state.trimEndTime = min(duration, state.maxRecordTime)
                state.playbackTime = 0
                return .none

            case .stopButtonTapped:
                let currentDuration = state.duration

                if currentDuration < State.minDuration {
                    let (oldUrl, trimmedUrl) = resetRecordingState(&state)
                    var urlsToCleanup = [oldUrl]
                    if let trimmedUrl = trimmedUrl {
                        urlsToCleanup.append(trimmedUrl)
                    }
                    state.destination = .minLengthAlert(.minLengthReached)
                    return .merge(
                        .cancel(id: ClockCancellable()),
                        .run { send in
                            await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: true)
                        },
                        cleanupFiles(urlsToCleanup, context: "stopButtonTapped - too short")
                    )
                }

                state.showProgressView = true
                return .merge(
                    .cancel(id: ClockCancellable()),
                    .run { [duration = state.duration, maxRecordTime = state.maxRecordTime] send in
                        // Stop recording but keep session active until modal is dismissed or confirmTrim
                        await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: false)
                        let finalTime: TimeInterval
                        if let currentTime = await audioRecorder.currentTimeWithEngine() {
                            finalTime = min(currentTime, maxRecordTime)
                        } else {
                            finalTime = min(duration, maxRecordTime)
                        }
                        await send(.finalRecordingTime(finalTime))
                    }
                )

            case .timerUpdated:
                guard let startDate = state.startDate else {
                    return .none
                }
                let calculatedDuration = date.now.timeIntervalSince(startDate)
                state.duration = min(calculatedDuration, state.maxRecordTime)
                if state.duration >= state.maxRecordTime {
                    let maxMinutes = Int(state.maxRecordTime / 60)
                    state.destination = .maxTimeReachedAlert(.maxTimeReached(maxMinutes: maxMinutes))
                    return .merge(
                        .cancel(id: ClockCancellable()),
                        .run { send in
                            await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: false)
                        }
                    )
                }
                return .run { send in
                    await send(.updateSamples(await audioRecorder.powerLevelsWithEngine()))
                }

            case .updateSamples(let powerLevels):
                guard state.duration > 0.3 else {
                    return .none
                }
                let normalizedPower = max(0.01, min(1.0, CGFloat(powerLevels.peakPower + 40) / 40))
                if state.duration < 0.5 && normalizedPower > 0.8 {
                    return .none
                }
                state.samples[state.duration] = normalizedPower
                return .none

            case .setupAudioPlayer:
                return .run { [url = state.url, log = self.log] _ in
                    do {
                        let player = try await MainActor.run {
                            try AVAudioPlayer(contentsOf: url)
                        }
                        await MainActor.run {
                            _ = player.prepareToPlay()
                        }
                    } catch {
                        log.telemetry.error(error, message: "Failed to setup audio player for preview")
                    }
                }

            case .playPreview:
                state.isPlaying = true
                return .run { [url = state.url, trimStartTime = state.trimStartTime, trimEndTime = state.trimEndTime] send in
                    await handlePlayback(
                        url: url,
                        trimStartTime: trimStartTime,
                        trimEndTime: trimEndTime,
                        send: send
                    )
                }
                .cancellable(id: PlaybackTimerCancellable())

            case .pausePreview:
                state.isPlaying = false
                return .cancel(id: PlaybackTimerCancellable())

            case .playbackTimeUpdated(let time):
                state.playbackTime = max(state.trimStartTime, min(time, state.trimEndTime))
                return .none

            case .confirmTrim:
                // Continue/Finish: deactivate session as recording is complete
                state.showProgressView = true
                if state.title.isEmpty || state.title == "Untitled" {
                    state.title = ChatAudio.audioTitleFormatter.string(from: date.now) // Set default title
                }

                // Validate trim times before attempting trim
                let startTime = state.trimStartTime
                let endTime = state.trimEndTime
                let trimmedDuration = endTime - startTime
                guard startTime >= 0, endTime > startTime, endTime <= state.duration, trimmedDuration >= State.minDuration else {
                    let error = AudioTrimmingError.invalidTrimTimes(
                        startTime: startTime,
                        endTime: endTime,
                        duration: state.duration,
                        trimmedDuration: trimmedDuration
                    )
                    log.telemetry.error(error, message: "Invalid trim times before confirm - trimmed duration must be at least \(State.minDuration) seconds")
                    state.showProgressView = false
                    return .send(.trimAudioCompleted(.failure(AnyError(AudioTrimmingError.exportFailed("Trimmed duration must be at least \(State.minDuration) seconds")))))
                }

                return .merge(
                    .run { _ in
                        await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: true)
                    },
                    .run { [url = state.url, startTime = startTime, endTime = endTime, title = state.title, titleCharCountLimit = state.titleCharCountLimit] send in
                        do {
                            let trimmedUrl = try await trimAudio(
                                inputUrl: url,
                                startTime: startTime,
                                endTime: endTime,
                                title: title,
                                titleCharCountLimit: titleCharCountLimit
                            )
                            await send(.trimAudioCompleted(.success(trimmedUrl)))
                        } catch {
                            log.telemetry.error(error, message: "Audio trimming failed - error during trim operation")
                            await send(.trimAudioCompleted(.failure(AnyError(error))))
                        }
                    }
                )

            case .trimAudioCompleted(.success(let trimmedUrl)):
                state.showProgressView = false
                state.isTrimming = false
                state.trimmedUrl = trimmedUrl
                let trimmedDuration = state.trimEndTime - state.trimStartTime
                let originalUrl = state.url
                let audioRecording = AudioRecording(
                    date: state.date,
                    duration: trimmedDuration,
                    title: state.title,
                    url: trimmedUrl
                )
                return .concatenate(
                    cleanupFiles([originalUrl], context: "trimAudioCompleted success"),
                    .send(.delegate(.didFinish(.success(audioRecording))))
                )

            case .trimAudioCompleted(.failure(let error)):
                state.showProgressView = false
                log.telemetry.error(error, message: "Audio trimming completed with failure")
                return .merge(
                    cleanupFiles([state.url], context: "trimAudioCompleted failure"),
                    .send(.delegate(.didFinish(.failure(error))))
                )

            case .teardownAudioPlayer:
                state.isPlaying = false
                return .cancel(id: PlaybackTimerCancellable())

            case .clearUntitledTitle:
                if state.title == "Untitled" {
                    state.title = ""
                }
                return .none

            case .dragStartHandleStarted:
                guard !state.isDraggingStartHandle else { return .none }
                state.isDraggingStartHandle = true
                state.dragOriginalStartTime = state.trimStartTime
                state.dragOriginalEndTime = state.trimEndTime
                return .none

            case .dragStartHandleChanged(let translation, let totalWidth):
                guard state.isDraggingStartHandle, state.duration > 0 else { return .none }
                let translationRatio = translation / totalWidth
                let delta = translationRatio * state.duration
                let originalStartTime = state.dragOriginalStartTime
                let originalEndTime = state.dragOriginalEndTime

                let newStartTime = originalStartTime + delta

                let maxStartTime = originalEndTime - State.minDuration
                let clampedStartTime = max(0, min(newStartTime, maxStartTime))

                let maxEndTime = min(state.duration, state.maxRecordTime)
                let finalMaxStartTime = maxEndTime - State.minDuration
                let finalStartTime = min(clampedStartTime, finalMaxStartTime)

                state.trimStartTime = finalStartTime
                return .none

            case .dragStartHandleEnded:
                state.isDraggingStartHandle = false
                return .none

            case .dragEndHandleStarted:
                guard !state.isDraggingEndHandle else { return .none }
                state.isDraggingEndHandle = true
                state.dragOriginalStartTime = state.trimStartTime
                state.dragOriginalEndTime = state.trimEndTime
                return .none

            case .dragEndHandleChanged(let translation, let totalWidth):
                guard state.isDraggingEndHandle, state.duration > 0 else { return .none }
                let translationRatio = translation / totalWidth
                let originalStartRatio = state.dragOriginalStartTime / state.duration
                let originalEndRatio = state.dragOriginalEndTime / state.duration

                let minEndRatio = originalStartRatio + (State.minDuration / state.duration)
                let newEndRatio = max(minEndRatio, originalEndRatio + translationRatio)
                let newEndTime = newEndRatio * state.duration

                let maxEndTime = min(state.duration, state.maxRecordTime)
                let clampedEndTime = min(newEndTime, maxEndTime)
                state.trimEndTime = clampedEndTime
                if state.trimEndTime - state.trimStartTime < State.minDuration {
                    state.trimEndTime = min(maxEndTime, state.trimStartTime + State.minDuration)
                }
                return .none

            case .dragEndHandleEnded:
                state.isDraggingEndHandle = false
                return .none

            case .dragSelectionStarted:
                guard !state.isDraggingSelection && !state.isDraggingStartHandle && !state.isDraggingEndHandle else { return .none }
                state.isDraggingSelection = true
                state.dragOriginalStartTime = state.trimStartTime
                state.dragOriginalEndTime = state.trimEndTime
                return .none

            case .dragSelectionChanged(let translation, let totalWidth):
                guard state.isDraggingSelection, state.duration > 0 else { return .none }
                let translationRatio = translation / totalWidth
                let delta = translationRatio * state.duration
                let originalStartTime = state.dragOriginalStartTime
                let originalEndTime = state.dragOriginalEndTime
                let maxEndTime = min(state.duration, state.maxRecordTime)

                let startConsumed = originalStartTime - max(0, originalStartTime + delta)
                let endConsumed = originalEndTime - min(maxEndTime, originalEndTime + delta)

                // Use the minimum absolute value (the one that hits boundary first)
                let consumed = abs(startConsumed) < abs(endConsumed) ? startConsumed : endConsumed

                // Both start and end move by the same amount (selection size stays constant)
                state.trimStartTime = originalStartTime - consumed
                state.trimEndTime = originalEndTime - consumed
                return .none

            case .dragSelectionEnded:
                state.isDraggingSelection = false
                return .none

            case .cancelTrim:
                let (oldUrl, trimmedUrl) = resetRecordingState(&state)
                var urlsToCleanup = [oldUrl]
                if let trimmedUrl = trimmedUrl {
                    urlsToCleanup.append(trimmedUrl)
                }
                return .merge(
                    .cancel(id: ClockCancellable()),
                    .cancel(id: PlaybackTimerCancellable()),
                    .run { send in
                        await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: false)
                    },
                    cleanupFiles(urlsToCleanup, context: "cancelTrim")
                )

            case .destination(.presented(.minLengthAlert(.ok))):
                state.destination = nil
                return .none

            case .destination(.presented(.maxTimeReachedAlert(.ok))):
                state.destination = nil
                // Proceed to trimming view after dismissing alert
                state.showProgressView = true
                return .run { [duration = state.duration, maxRecordTime = state.maxRecordTime] send in
                    let finalTime: TimeInterval
                    if let currentTime = await audioRecorder.currentTimeWithEngine() {
                        finalTime = min(currentTime, maxRecordTime)
                    } else {
                        finalTime = min(duration, maxRecordTime)
                    }
                    await send(.finalRecordingTime(finalTime))
                }

            case .destination:
                return .none

            case .binding:
                return .none

            case .delegate:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
    }
}

extension AlertState where Action == ChatAudioRecorder.Destination.Alert {
    static var minLengthReached: Self {
        Self {
            TextState(L10n.FeatureCreateClip.recordingTooShortErrorTitle)
        } actions: {
            ButtonState(action: .ok) {
                TextState(L10n.FeatureCreateClip.ok)
            }
        } message: {
            TextState(L10n.FeatureCreateClip.recordingTooShortErrorSubtitle)
        }
    }

    static func maxTimeReached(maxMinutes: Int) -> Self {
        Self {
            TextState(L10n.FeatureCreateClip.recordingLimitReachedErrorMessage(maxMinutes))
        } actions: {
            ButtonState(action: .ok) {
                TextState(L10n.FeatureCreateClip.ok)
            }
        } message: {
            TextState(L10n.FeatureCreateClip.recordingLimitReachedErrorMessage(maxMinutes))
        }
    }
}

extension ChatAudioRecorder.Action: Equatable {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.task, .task),
             (.startRecordingTapped, .startRecordingTapped),
             (.stopButtonTapped, .stopButtonTapped),
             (.timerUpdated, .timerUpdated),
             (.setupAudioPlayer, .setupAudioPlayer),
             (.playPreview, .playPreview),
             (.pausePreview, .pausePreview),
             (.teardownAudioPlayer, .teardownAudioPlayer),
             (.clearUntitledTitle, .clearUntitledTitle),
             (.dragStartHandleStarted, .dragStartHandleStarted),
             (.dragStartHandleEnded, .dragStartHandleEnded),
             (.dragEndHandleStarted, .dragEndHandleStarted),
             (.dragEndHandleEnded, .dragEndHandleEnded),
             (.dragSelectionStarted, .dragSelectionStarted),
             (.dragSelectionEnded, .dragSelectionEnded),
             (.confirmTrim, .confirmTrim),
             (.cancelTrim, .cancelTrim):
            return true
        case let (.delegate(lhs), .delegate(rhs)):
            return lhs == rhs
        case let (.finalRecordingTime(lhs), .finalRecordingTime(rhs)):
            return lhs == rhs
        case let (.updateSamples(lhs), .updateSamples(rhs)):
            return lhs == rhs
        case let (.playbackTimeUpdated(lhs), .playbackTimeUpdated(rhs)):
            return lhs == rhs
        case let (.dragStartHandleChanged(lhsTranslation, lhsWidth), .dragStartHandleChanged(rhsTranslation, rhsWidth)):
            return lhsTranslation == rhsTranslation && lhsWidth == rhsWidth
        case let (.dragEndHandleChanged(lhsTranslation, lhsWidth), .dragEndHandleChanged(rhsTranslation, rhsWidth)):
            return lhsTranslation == rhsTranslation && lhsWidth == rhsWidth
        case let (.dragSelectionChanged(lhsTranslation, lhsWidth), .dragSelectionChanged(rhsTranslation, rhsWidth)):
            return lhsTranslation == rhsTranslation && lhsWidth == rhsWidth
        case let (.binding(lhs), .binding(rhs)):
            return lhs == rhs
        case let (.audioRecorderDidFinish(lhs), .audioRecorderDidFinish(rhs)):
            switch (lhs, rhs) {
            case let (.success(lhsBool), .success(rhsBool)):
                return lhsBool == rhsBool
            case let (.failure(lhsError), .failure(rhsError)):
                return lhsError == rhsError
            default:
                return false
            }
        case let (.trimAudioCompleted(lhs), .trimAudioCompleted(rhs)):
            switch (lhs, rhs) {
            case let (.success(lhsUrl), .success(rhsUrl)):
                return lhsUrl == rhsUrl
            case let (.failure(lhsError), .failure(rhsError)):
                return lhsError == rhsError
            default:
                return false
            }
        case let (.destination(lhs), .destination(rhs)):
            switch (lhs, rhs) {
            case (.presented, .presented):
                return true
            case (.dismiss, .dismiss):
                return true
            default:
                return false
            }
        default:
            return false
        }
    }
}
