import AVFoundation
import APIClient
import ComposableArchitecture
import Errors
import Localization
import UserDefaultsClient
import Utilities

@Reducer
public struct ChatAudio {
    public enum ProcessingError: LocalizedError {
        case fileNotFound
        case invalidDuration

        public var errorDescription: String? {
            switch self {
            case .fileNotFound:
                return "Audio file does not exist"
            case .invalidDuration:
                return "Invalid audio duration"
            }
        }
    }

    public enum UploadError: LocalizedError {
        case moderationRejected(String)

        public var errorDescription: String? {
            switch self {
            case .moderationRejected(let message):
                return message
            }
        }
    }
    private let log = Logger(category: "ChatAudio")
    @Reducer(state: .equatable)
    public enum Destination {
        case documentPicker
        case termsOfService
        case recording(ChatAudioRecorder)
        case libraryClipPicker(LibraryClipPicker)
    }

    public enum ChatAudioErrorState: Equatable {
        case recordingFailed(String)
        case fileProcessingFailed(String, isRetryable: Bool)
        case trimmingFailed(String)
        case uploadFailed(String, isRetryable: Bool)
        case moderationRejected(String)

        public var isRetryable: Bool {
            switch self {
            case .recordingFailed, .trimmingFailed:
                return true
            case .fileProcessingFailed(_, let retryable), .uploadFailed(_, let retryable):
                return retryable
            case .moderationRejected:
                return false
            }
        }

        public var errorMessage: String {
            switch self {
            case .recordingFailed(let message),
                    .fileProcessingFailed(let message, _),
                    .trimmingFailed(let message),
                    .uploadFailed(let message, _),
                    .moderationRejected(let message):
                return message
            }
        }
    }

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

        // Audio file state (for UI display in ChatBar)
        public var audioFileName: String?
        public var audioFileDuration: TimeInterval?

        // Upload state (managed by parent, but displayed here)
        public var uploadProgress: Double = 0.0
        public var isUploading: Bool = false
        public var uploadCompleted: Bool = false

        // TOS state
        public var pendingActionAfterTOS: PendingAction?

        public enum PendingAction: Equatable {
            case upload
            case record
        }

        public var showMicrophonePermissionAlert: Bool = false

        public var errorState: ChatAudioErrorState?

        // Store AudioRecording for retry
        public var pendingAudioRecording: AudioRecording?
        // Store original file URL for retry when processing fails
        public var pendingFileUrl: URL?

        @Shared(.inMemory(.billingInfo)) public var billingInfo: SubscriptionInfoResponse?

        public var hasActiveAudio: Bool {
            audioFileName != nil || errorState != nil
        }

        public init() {}
    }

    public enum Action: BindableAction {
        public enum Delegate: Equatable {
            case uploadCompleted(String) // clipId - parent needs this to include in message
            case clipSelectedFromLibrary(ClipSnippet)
        }

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

        // User actions
        case uploadTapped
        case recordTapped
        case libraryTapped
        case checkMicrophonePermissionResponse(Bool)
        case closeAudioTapped

        // File processing
        case processAudioFile(Result<URL, AnyError>)
        case processAudioFileDidFinish(URL, String, Result<TimeInterval, AnyError>) // Using TimeInterval instead of CMTime for Equatable

        // Upload actions
        case startUpload(AudioRecording)
        case uploadEvent(ChatAudioClient.AudioUploadStatus)
        case uploadCompleted(clipId: String) // Internal: sends delegate after clip initialization
        case uploadError(Error)
        case resetUploadCompleted // Internal: reset completed state after showing green

        // TOS actions
        case termsOfServiceAccepted
        case termsOfServiceDismissed

        // Permission actions
        case microphonePermissionAlertDismissed

        // Error actions
        case retryUpload
        case dismissError
    }

    @Dependency(\.date) private var date
    @Dependency(\.temporaryDirectory) private var temporaryDirectory
    @Dependency(\.uuid) private var uuid
    @Dependency(UserDefaultsClient.self) private var userDefaults
    @Dependency(ChatAudioClient.self) private var chatAudioClient
    @Dependency(\.continuousClock) private var clock

    static let audioTitleFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "MM-dd-yyyy_HH:mm:ss"
        return formatter
    }()

    static func maxRecordTime(billingInfo: SubscriptionInfoResponse?) -> TimeInterval {
        if let billingInfo {
            return TimeInterval(billingInfo.audioUploadLimits.max)
        } else {
            return 60 // Fallback to 60
        }
    }


    private var newRecordingAudio: ChatAudioRecorder.State {
        ChatAudioRecorder.State(
            date: date.now,
            url: temporaryDirectory()
                .appendingPathComponent(uuid().uuidString)
                .appendingPathExtension("m4a")
        )
    }

    public init() {}

    @Dependency(\.audioRecorder) private var audioRecorder

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce<State, Action> { state, action in
            switch action {
            case .task:
                return .run { _ in
                    // Pause any playing audio when starting
                    @Dependency(\.omniplayerClient.pauseCurrentClip) var pauseCurrentClip
                    pauseCurrentClip()
                }

            case .uploadTapped:
                guard userDefaults.hasAcceptedAudioUploadTOS else {
                    state.pendingActionAfterTOS = .upload
                    state.destination = .termsOfService
                    return .none
                }
                state.destination = .documentPicker
                return .none

            case .recordTapped:
                if case .recording(let recordingState) = state.destination, recordingState.hasStartedRecording {
                    return .none
                }

                guard userDefaults.hasAcceptedAudioUploadTOS else {
                    state.pendingActionAfterTOS = .record
                    state.destination = .termsOfService
                    return .none
                }
                return .run { send in
                    let granted = await AVAudioApplication.requestRecordPermission()
                    await send(.checkMicrophonePermissionResponse(granted))
                }

            case .libraryTapped:
                state.destination = .libraryClipPicker(LibraryClipPicker.State())
                return .none

            case .checkMicrophonePermissionResponse(let granted):
                if granted {
                    state.destination = .recording(newRecordingAudio)
                } else {
                    state.showMicrophonePermissionAlert = true
                }
                return .none

            case .closeAudioTapped:
                state.audioFileName = nil
                state.audioFileDuration = nil
                state.uploadProgress = 0.0
                state.isUploading = false
                state.uploadCompleted = false
                state.errorState = nil
                state.pendingAudioRecording = nil
                state.pendingFileUrl = nil
                state.destination = nil
                return .none

            case .processAudioFile(.success(let originalUrl)):
                // Store original URL for potential retry
                state.pendingFileUrl = originalUrl
                let tempUrl = temporaryDirectory()
                    .appendingPathComponent(uuid().uuidString)
                    .appendingPathExtension(originalUrl.pathExtension)
                let fileName = originalUrl.deletingPathExtension().lastPathComponent

                return .run { send in
                    do {
                        // Validate file exists and is accessible
                        guard FileManager.default.fileExists(atPath: originalUrl.path) else {
                            throw ProcessingError.fileNotFound
                        }

                        try FileManager.default.copyItem(at: originalUrl, to: tempUrl)

                        let asset = AVURLAsset(url: tempUrl)
                        let duration = try await asset.load(.duration)
                        let durationSeconds = duration.seconds

                        await send(.processAudioFileDidFinish(tempUrl, fileName, .success(durationSeconds)))
                    } catch {
                        try? FileManager.default.removeItem(at: tempUrl)
                        log.telemetry.error(error, message: "Failed to process audio file - \(error.localizedDescription)")
                        await send(.processAudioFileDidFinish(tempUrl, fileName, .failure(AnyError(error))))
                    }
                }

            case .processAudioFile(.failure(let error)):
                log.telemetry.error(error, message: "Failed to process audio file")
                state.pendingFileUrl = nil // Clear since document picker failed
                state.errorState = .fileProcessingFailed(L10n.FeatureCreateClip.createFileFailed, isRetryable: true)
                return .none

            case .processAudioFileDidFinish(let tempUrl, let fileName, .success(let duration)):
                state.audioFileName = fileName.isEmpty ? "Recording" : fileName
                state.audioFileDuration = duration
                state.destination = nil
                state.pendingFileUrl = nil

                if duration <= 0 {
                    let errorMessage = L10n.FeatureCreateClip.createFileFailed
                    log.telemetry.error(
                        ProcessingError.invalidDuration,
                        message: "Audio file has invalid duration (zero or negative): \(duration)"
                    )
                    state.errorState = .fileProcessingFailed(errorMessage, isRetryable: true)
                    return .none
                }

                if duration < ChatAudioRecorder.State.minDuration {
                    let errorMessage = L10n.FeatureCreateClip.recordingTooShortErrorSubtitle
                    log.telemetry.error(
                        ProcessingError.invalidDuration,
                        message: "Audio file duration is less than minimum required duration: \(duration)"
                    )
                    state.errorState = .fileProcessingFailed(errorMessage, isRetryable: true)
                    return .none
                }

                let maxRecordTime = Self.maxRecordTime(billingInfo: state.billingInfo)
                if duration > maxRecordTime {
                    let maxMinutes = Int(maxRecordTime / 60)
                    let errorMessage = L10n.FeatureCreateClip.recordingLimitReachedErrorMessage(maxMinutes)
                    log.telemetry.error(
                        ProcessingError.invalidDuration,
                        message: "Audio file exceeds max time: \(duration) > \(maxRecordTime)"
                    )
                    state.errorState = .fileProcessingFailed(errorMessage, isRetryable: false)
                    return .none
                }

                let audioRecording = AudioRecording(
                    date: date.now,
                    duration: duration,
                    title: fileName.isEmpty ? Self.audioTitleFormatter.string(from: date.now) : fileName,
                    url: tempUrl
                )
                // Start upload immediately after file is processed
                return .send(.startUpload(audioRecording))

            case .processAudioFileDidFinish(_, _, .failure(let error)):
                log.telemetry.error(error, message: "Failed to finish processing audio file")
                state.errorState = .fileProcessingFailed(L10n.FeatureCreateClip.createFileFailed, isRetryable: true)
                return .none

            case .termsOfServiceAccepted:
                state.destination = nil
                let pendingAction = state.pendingActionAfterTOS
                state.pendingActionAfterTOS = nil
                return .run { send in
                    await userDefaults.setHasAcceptedAudioUploadTOS(true)
                    // Re-trigger the original action after accepting TOS
                    switch pendingAction {
                    case .upload:
                        await send(.uploadTapped)
                    case .record:
                        await send(.recordTapped)
                    case .none:
                        break
                    }
                }

            case .termsOfServiceDismissed:
                state.destination = nil
                state.pendingActionAfterTOS = nil
                return .none

            case .microphonePermissionAlertDismissed:
                state.showMicrophonePermissionAlert = false
                return .none

            case .destination(.presented(.recording(.delegate(.didFinish(.success(let audioRecording)))))):
                state.destination = nil
                state.audioFileName = audioRecording.title
                state.audioFileDuration = audioRecording.duration
                return .send(.startUpload(audioRecording))

            case .destination(.presented(.recording(.delegate(.didFinish(.failure(let error)))))):
                state.destination = nil
                if let trimmingError = error.underlying as? ChatAudioRecorder.AudioTrimmingError {
                    log.telemetry.error(error, message: "Audio trimming failed - \(trimmingError)")
                    state.errorState = .trimmingFailed(L10n.FeatureCreateClip.createFileFailed)
                } else if error.underlying is ChatAudioRecorder.AudioRecorderError {
                    log.telemetry.error(error, message: "Audio recording failed")
                    state.errorState = .recordingFailed(L10n.FeatureCreateClip.recordingFailed)
                } else {
                    log.telemetry.error(error, message: "Recording/trimming failed - unknown error type")
                    state.errorState = .recordingFailed(L10n.FeatureCreateClip.recordingFailed)
                }
                return .none

            case .destination(.presented(.libraryClipPicker(.delegate(.didSelectSnippet(let snippet))))):
                state.destination = nil
                return .send(.delegate(.clipSelectedFromLibrary(snippet)))

            case .destination(.dismiss):
                if case .recording(let recordingState) = state.destination {
                    let recordingUrl = recordingState.url
                    let trimmedUrl = recordingState.trimmedUrl
                    let isRecording = recordingState.hasStartedRecording
                    state.destination = nil
                    var urlsToCleanup = [recordingUrl]
                    if let trimmedUrl = trimmedUrl {
                        urlsToCleanup.append(trimmedUrl)
                    }
                    return .run { send in
                        if isRecording {
                            await audioRecorder.stopRecordingWithEngine(shouldDeactivateSession: true)
                        }
                        for url in urlsToCleanup {
                            try? FileManager.default.removeItem(at: url)
                        }
                    }
                }
                state.destination = nil
                return .none

            case .startUpload(let audioRecording):
                state.errorState = nil
                state.pendingAudioRecording = audioRecording // Store for retry
                state.isUploading = true
                state.uploadProgress = 0.0
                state.uploadCompleted = false // Reset for new upload
                return .run { send in
                    do {
                        for try await event in chatAudioClient.uploadAudio(audioRecording) {
                            await send(.uploadEvent(event))
                        }
                    } catch {
                        log.telemetry.error(error, message: "Failed to start audio upload stream")
                        await send(.uploadError(error))
                    }
                }

            case .uploadEvent(let status):
                switch status {
                case .uploadEvent(let event):
                    switch event {
                    case .updateProgress(let progress):
                        state.uploadProgress = progress
                        return .none
                    case .success:
                        state.uploadProgress = 1.0
                        state.isUploading = false
                        state.uploadCompleted = true
                        return .merge(
                            // Fade away progress bar after showing green for 1 second
                            .run { send in
                                try await clock.sleep(for: .seconds(1))
                                await send(.resetUploadCompleted)
                            }
                        )
                    case .failure(let error):
                        log.telemetry.error(error, message: "Audio upload event failed")
                        return .send(.uploadError(error))
                    }
                case .status(let uploadStatus):
                    if let errorMessage = uploadStatus.errorMessage {
                        // Moderation rejection - not retryable
                        let error = UploadError.moderationRejected(errorMessage)
                        log.telemetry.error(error, message: "Audio upload rejected by moderation")
                        state.errorState = .moderationRejected(errorMessage)
                        state.isUploading = false
                        return .none
                    }

                    switch uploadStatus.status {
                    case .complete:
                        return .run { send in
                            do {
                                let clipId = try await chatAudioClient.initializeClip(uploadRequestId: uploadStatus.id)
                                await send(.uploadCompleted(clipId: clipId))
                            } catch {
                                log.telemetry.error(error, message: "Failed to initialize clip after upload completion")
                                await send(.uploadError(error))
                            }
                        }
                    default:
                        // Continue polling
                        return .none
                    }
                }

            case .uploadCompleted(let clipId):
                state.pendingAudioRecording = nil // Clear after successful upload
                return .send(.delegate(.uploadCompleted(clipId)))

            case .uploadError(let error):
                state.isUploading = false
                if let audioUploadError = error as? ChatAudioClient.AudioUploadError {
                    let errorMessage: String
                    let isRetryable: Bool
                    switch audioUploadError {
                    case .fileCreationFailed:
                        errorMessage = L10n.FeatureCreateClip.createFileFailed
                        isRetryable = true
                        log.telemetry.error(error, message: "Audio upload failed - file creation failed")
                    case .verificationFailed:
                        errorMessage = L10n.FeatureCreateClip.verifyFailed
                        isRetryable = true
                        log.telemetry.error(error, message: "Audio upload failed - verification failed")
                    case .statusCheckFailed:
                        errorMessage = L10n.FeatureCreateClip.statusCheckFailed
                        isRetryable = true
                        log.telemetry.error(error, message: "Audio upload failed - status check failed")
                    case .timeout:
                        errorMessage = L10n.FeatureCreateClip.audioUploadFailed
                        isRetryable = true
                        log.telemetry.error(error, message: "Audio upload failed - timeout")
                    }
                    state.errorState = .uploadFailed(errorMessage, isRetryable: isRetryable)
                } else if error is ChatAudioClient.InitializeClipError {
                    log.telemetry.error(error, message: "Audio upload failed - clip initialization error")
                    state.errorState = .uploadFailed(L10n.FeatureCreateClip.audioUploadFailed, isRetryable: true)
                } else {
                    // Generic upload error
                    log.telemetry.error(error, message: "Audio upload failed - unknown error type")
                    state.errorState = .uploadFailed(L10n.FeatureCreateClip.audioUploadFailed, isRetryable: true)
                }
                return .none

            case .retryUpload:
                guard let errorState = state.errorState else { return .none }
                state.errorState = nil

                switch errorState {
                case .recordingFailed:
                    return .send(.recordTapped)

                case .fileProcessingFailed(_, let isRetryable):
                    if isRetryable, let fileUrl = state.pendingFileUrl {
                        return .send(.processAudioFile(.success(fileUrl)))
                    } else {
                        return .none
                    }

                case .trimmingFailed, .moderationRejected:
                    return .none

                case .uploadFailed(_, let isRetryable):
                    if isRetryable, let audioRecording = state.pendingAudioRecording {
                        return .send(.startUpload(audioRecording))
                    } else {
                        return .none
                    }
                }

            case .dismissError:
                state.errorState = nil
                return .none

            case .resetUploadCompleted:
                state.uploadCompleted = false
                return .none

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

extension ChatAudio.Action: Equatable {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case (.task, .task),
             (.uploadTapped, .uploadTapped),
             (.recordTapped, .recordTapped),
             (.libraryTapped, .libraryTapped),
             (.closeAudioTapped, .closeAudioTapped):
            return true
        case let (.binding(lhs), .binding(rhs)):
            return lhs == rhs
        case let (.delegate(lhs), .delegate(rhs)):
            return lhs == rhs
        case let (.processAudioFile(lhs), .processAudioFile(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 (.processAudioFileDidFinish(lhsUrl, lhsName, lhsResult), .processAudioFileDidFinish(rhsUrl, rhsName, rhsResult)):
            guard lhsUrl == rhsUrl && lhsName == rhsName else { return false }
            switch (lhsResult, rhsResult) {
            case let (.success(lhsDuration), .success(rhsDuration)):
                return lhsDuration == rhsDuration
            case let (.failure(lhsError), .failure(rhsError)):
                return lhsError == rhsError
            default:
                return false
            }
        case let (.uploadCompleted(lhs), .uploadCompleted(rhs)):
            return lhs == rhs
        case (.resetUploadCompleted, .resetUploadCompleted):
            return true
        case let (.startUpload(lhs), .startUpload(rhs)):
            return lhs == rhs
        case let (.uploadEvent(lhs), .uploadEvent(rhs)):
            switch (lhs, rhs) {
            case let (.uploadEvent(lhsEvent), .uploadEvent(rhsEvent)):
                switch (lhsEvent, rhsEvent) {
                case let (.updateProgress(lhsProgress), .updateProgress(rhsProgress)):
                    return lhsProgress == rhsProgress
                case (.success, .success):
                    return true
                case let (.failure(lhsError), .failure(rhsError)):
                    return lhsError.localizedDescription == rhsError.localizedDescription
                default:
                    return false
                }
            case let (.status(lhsStatus), .status(rhsStatus)):
                return lhsStatus == rhsStatus
            default:
                return false
            }
        case let (.uploadError(lhs), .uploadError(rhs)):
            return lhs.localizedDescription == rhs.localizedDescription
        case (.termsOfServiceAccepted, .termsOfServiceAccepted),
             (.termsOfServiceDismissed, .termsOfServiceDismissed),
             (.retryUpload, .retryUpload),
             (.dismissError, .dismissError),
             (.microphonePermissionAlertDismissed, .microphonePermissionAlertDismissed):
            return true
        case let (.checkMicrophonePermissionResponse(lhs), .checkMicrophonePermissionResponse(rhs)):
            return lhs == rhs
         case let (.destination(lhs), .destination(rhs)):
             // PresentationAction comparison - just compare the cases, not the inner actions
             // This is sufficient for Equatable conformance since PresentationAction
             // is mainly used for navigation state, not deep value comparison
             switch (lhs, rhs) {
             case (.presented, .presented):
                 return true
             case (.dismiss, .dismiss):
                 return true
             default:
                 return false
             }
        default:
            return false
        }
    }
}
