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

@Reducer
public struct ChatAudioRecorder {
    @ObservableState
    public struct State: Equatable, Sendable {

        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 task
        case startRecordingTapped
        case stopButtonTapped
        case audioRecorderDidFinish(Result<Bool, AnyError>)
        case finalRecordingTime(TimeInterval)
        case timerUpdated
        case updateSamples(PowerLevels)

        // Trimming actions
        case trimStartChanged(TimeInterval)
        case trimEndChanged(TimeInterval)
        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 {
        let player = try? await MainActor.run {
            let player = try AVAudioPlayer(contentsOf: url)
            player.prepareToPlay()
            return player
        }
        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
    }

    private enum AudioRecorderError: LocalizedError {
        case recordingFailed

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

    private enum AudioTrimmingError: LocalizedError {
        case audioTrackMissing
        case compositionFailed
        case exportSessionFailed
        case exportFailed(String)

        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)"
            }
        }
    }

    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 }
                                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)):
                // TODO: Add error state to display recording failure to user
                state.hasStartedRecording = false
                state.startDate = nil
                return .merge(
                    .cancel(id: ClockCancellable()),
                    .send(.delegate(.didFinish(.failure(AnyError(AudioRecorderError.recordingFailed)))))
                )

            case .audioRecorderDidFinish(.failure(let error)):
                // TODO: Add error state to display recording error to user
                state.hasStartedRecording = false
                state.startDate = nil
                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:
                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 {
                    return .send(.stopButtonTapped)
                }
                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 .trimStartChanged(let startTime):
                let maxEnd = min(state.duration, state.maxRecordTime)
                let clampedStart = max(0, min(startTime, state.trimEndTime - 0.1))
                state.trimStartTime = clampedStart
                if state.trimEndTime > maxEnd {
                    state.trimEndTime = maxEnd
                }
                return .none

            case .trimEndChanged(let endTime):
                let maxEnd = min(state.duration, state.maxRecordTime)
                let clampedEnd = min(maxEnd, max(state.trimStartTime + 0.1, endTime))
                state.trimEndTime = clampedEnd
                return .none

            case .setupAudioPlayer:
                return .run { [url = state.url] _ in
                    let player = try? await MainActor.run {
                        try AVAudioPlayer(contentsOf: url)
                    }
                    await MainActor.run {
                        _ = player?.prepareToPlay()
                    }
                }

            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
                }
                return .merge(
                    .run { _ in
                        await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: true)
                    },
                    .run { [url = state.url, startTime = state.trimStartTime, endTime = state.trimEndTime, 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 {
                            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
                // TODO: Add error state to display trimming failure to user
                // TODO: Validate trimmed audio file before sending delegate (duration, file size, format)
                // Clean up recording file on 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 originalStartRatio = state.dragOriginalStartTime / state.duration
                let originalEndRatio = state.dragOriginalEndTime / state.duration
                let newStartRatio = max(0, min(originalStartRatio + translationRatio, originalEndRatio - 0.05))
                let newStartTime = newStartRatio * state.duration
                let selectionSize = (originalEndRatio - originalStartRatio) * state.duration
                let newEndTime = newStartTime + selectionSize

                if newEndTime <= state.maxRecordTime {
                    state.trimStartTime = newStartTime
                } else {
                    let clampedEndRatio = state.maxRecordTime / state.duration
                    let clampedStartRatio = max(0, clampedEndRatio - (originalEndRatio - originalStartRatio))
                    state.trimStartTime = clampedStartRatio * state.duration
                }
                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 newEndRatio = max(originalEndRatio + translationRatio, originalStartRatio + 0.05)
                let newEndTime = newEndRatio * state.duration

                let maxEndTime = min(state.duration, state.maxRecordTime)
                let clampedEndTime = min(newEndTime, maxEndTime)
                state.trimEndTime = clampedEndTime
                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
                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 .binding:
                return .none

            case .delegate:
                return .none
            }
        }
    }
}

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 (.trimStartChanged(lhs), .trimStartChanged(rhs)):
            return lhs == rhs
        case let (.trimEndChanged(lhs), .trimEndChanged(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
            }
        default:
            return false
        }
    }
}
