import AdamantiumClient
import APIClient
import AVKit
import CameraClient
import ComponentLibrary
import ComposableArchitecture
import FeatureBrandedAlert
import FeatureVideoTrimmer
import Localization
import PhotosUI
import PlayerClient
import SwiftUI
import Utilities

// swiftlint:disable file_length

let media = "media"

@Reducer
public struct CreateClipCamera {
    @CasePathable
    @dynamicMemberLookup
    public enum ClipMedia: Equatable {
        case image(ClipImageReducer.State)
        case video(ClipVideoReducer.State)
    }

    @Reducer(state: .equatable)
    public enum Destination {
        case mediaPicker
        case videoTrimmer(VideoTrimmer)
    }

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

        @ObservationStateIgnored @ObservedBox public var media: ClipMedia?
        public var isRecording: Bool = false

        public var didTapCapturePhoto = false
        public var didTapRecordVideo = false
        public var didTapFlipCamera = false
        @Shared(.appStorage(.lastCameraFlashToggleMode)) var flash = false
        @Shared(.appStorage(.hasSeenContextPrompt)) var hasSeenContextPrompt = false

        public var selectedImage: UIImage?
        public var selectedImageUrl: URL?
        public var selectedMovie: Movie?
        public var isShowingInteractionBlock: Bool = false
        public var mediaTransferProgress: Double = 0

        public var reusePrompt: Prompt?
        public var hCaptchaToken: String?
        public var isRemix: Bool?

        init(reusePrompt: Prompt? = nil, isRemix: Bool? = nil) {
            self.reusePrompt = reusePrompt
            self.isRemix = isRemix
        }
    }

    public enum Action {
        public enum Delegate {
            case generationResponse(Prompt, Result<[Clip], Error>)
            case submitVideo(String, String?, Bool?) // caption, hCaptchaToken, isRemix
            case showAlert(BrandedAlertStyle)
        }

        case image(ClipImageReducer.Action)
        case video(ClipVideoReducer.Action)

        case tookImage(UIImage)
        case tookVideo(URL, Float) // Captured Video URL, Target Frames per second
        case showVideoPreview(URL)
        case importedImage(UIImage)
        case importedVideo(URL)
        case reset

        case setDidTapCapturePhoto(Bool)
        case setDidTapRecordVideo(Bool)
        case setDidTapFlipCamera(Bool)
        case setFlash(Bool)
        case setSelectedImage(UIImage?)
        case setSelectedMovie(Movie?)
        case didSelectImage
        case didSelectMovie
        case setMediaTransferProgress(Double?)

        case takePhoto
        case setRecording(Bool)
        case flipCamera
        case toggleFlash
        case stopAudioIfPlaying

        case destination(PresentationAction<Destination.Action>)
        case presentMediaPicker
        case dismissMediaPicker
        case mediaPickerDidError
        case presentVideoTrimmer(URL)
        case dismissVideoTrimmer

        case generationResponse(Prompt, Result<[Clip], Error>)
        case delegate(Delegate)

        case submitVideo(String?)
        case submitImage(String?)
    }

    @Dependency(\.apiClientV2) private var api
    @Dependency(APIClient.self) private var apiClient
    @Dependency(PlayerClient.self) private var player
    @Dependency(AdamantiumClient.self) private var adamantiumClient

    public var body: some ReducerOf<Self> {
        Scope(state: \.media, action: \.self) {
            Scope(state: \.image, action: \.image) {
                ClipImageReducer()
            }
            Scope(state: \.video, action: \.video) {
                ClipVideoReducer()
            }
        }

        Reduce { state, action in
            switch action {
            case .reset:
                state = .init()
                return .none

            case .stopAudioIfPlaying:
                player.pause()
                return .none

            case let .tookImage(image):
                state.media = .image(.init(image: .local(image: image, sourceType: .takenByCamera)))
                state.destination = nil
                return .none

            case let .importedImage(image):
                state.media = .image(.init(image: .local(image: image, sourceType: .imported)))
                state.destination = nil
                return .none

            case .showVideoPreview(let videoURL):
                state.isShowingInteractionBlock = false
                state.media = .video(.init(video: videoURL))
                return .none

            case .tookVideo(let videoURL, let targetFPS):
                /*
                     iPhone 13 and above are unaffected,
                     iPhone 12 and and below show to have issues

                     The need for a separate target FPS here is because
                     all the recorded video will have a nominalFrameRate of 60 fps
                     but the actual frame rate of the video will be a scaled version of that
                     to match the speed at which it was recorded. This depends on device.

                     Clips that have an FPS below 60 should be retimed so they appear in
                     1x speed. However if they are close enough to 60 fps then
                     just let them pass. Currently the threshold is 55 fps
                 */

                if targetFPS < 55.0 {
                    state.isShowingInteractionBlock = true
                    return .run { send in
                        if let retimedVideoURL = try? await adamantiumClient.retimeVideo(videoURL, 60.0, targetFPS) {
                            await send(.showVideoPreview(retimedVideoURL))
                        } else {
                            await send(.showVideoPreview(videoURL))
                        }
                    }
                } else {
                    return .send(.showVideoPreview(videoURL))
                }

            case .importedVideo(let videoURL):
                return .send(.presentVideoTrimmer(videoURL))

            case .destination(.presented(.videoTrimmer(.delegate(.saveResponse(.failure))))):
                return .send(.dismissVideoTrimmer)

            case .destination(.presented(.videoTrimmer(.delegate(.saveResponse(.success(let trimmedURL)))))):
                state.media = .video(.init(video: trimmedURL))
                return .send(.dismissVideoTrimmer)

            case .destination(.presented(.videoTrimmer(.delegate(.showDurationError)))):
                return .concatenate(
                    .send(.dismissVideoTrimmer),
                    .run(operation: { send in
                        try await Task.sleep(for: .seconds(0.3))
                        await send(.delegate(.showAlert(.singleButtonAlert(.preset(.videoTooShort)))))
                    })
                )

            case .destination(.presented(.videoTrimmer)):
                return .none

            case .submitVideo(let hCaptchaToken):
                guard let videoState = state.media?.video else { return .none }
                state.hCaptchaToken = hCaptchaToken
                let caption = videoState.caption
                return .send(.delegate(.submitVideo(caption, hCaptchaToken, state.isRemix)))

            case .video(.loadVideoResponse(.failure(let error))):
                log.telemetry.error(error)
                state.selectedMovie = nil
                state.media = nil
                return .send(.presentMediaPicker)

            case .submitImage(let hCaptchaToken):
                state.hCaptchaToken = hCaptchaToken
                guard let imageState = state.media?.image else { return .none }
                switch imageState.image {
                case .local(let image, let sourceType):
                    guard let reformattedImage = CreateClipImageProcessing.resizeByInputRatio(
                        image: image,
                        sourceType: sourceType,
                        fitFillThreshold: 1.5, // a little less than 16:9; a little more than 4:3
                        targetSize: .portraitHD
                    )
                    else { return .none }

                    return .run { send in
                        await send(.image(.createImageFileResponse(reformattedImage, Result(catching: { try await apiClient.createImageFile() }))))
                    }

                case let .remote(uploadIds, uploadURLs):
                    return .send(.image(.complete(uploadIds: uploadIds, uploadURLs: uploadURLs)))
                }

            case let .image(.createImageFileResponse(image, .success(uploadRequest))):
                guard let fileData = image.jpegData(compressionQuality: 0.7) else { return .none }
                return .run(priority: .background) { send in
                    for try await event in apiClient.uploadData(fileData, uploadRequest) {
                        await send(.image(.uploadImageResponse([uploadRequest], .success(event))))
                    }
                } catch: { error, send in
                    await send(.image(.uploadImageResponse([uploadRequest], .failure(error))))
                }

            case let .image(.createImageFileResponse(_, .failure(error))):
                log.telemetry.error(error)
                return .none

            case .image(.uploadImageResponse(_, .success(.updateProgress))):
                // Progress updates are ignored in the Image upload flow
                return .none

            case let .image(.uploadImageResponse(uploadRequests, .success(.success))):
                return .run { send in
                    await send(.image(.complete(uploadIds: uploadRequests.map { $0.id }, uploadURLs: uploadRequests.map { $0.url })))
                }

            case .image(.uploadImageResponse(_, .failure(let error))):
                log.telemetry.error(error)
                return .none

            case let .image(.complete(uploadIds, uploadURLs)):
                guard let imageState = state.media?.image else { return .none }

                if imageState.caption.isEmpty && !state.hasSeenContextPrompt {
                    state.$hasSeenContextPrompt.withLock { $0 = true }
                    return .merge(
                        .send(.delegate(.showAlert(.singleButtonAlert(.preset(.addContext))))),
                        .send(.image(.setIsCreating(false)))
                    )
                }

                let prompt = Prompt(
                    title: "",
                    lyrics: "",
                    instrumental: false,
                    styles: "",
                    text: imageState.caption,
                    continueClipId: nil,
                    continueAt: nil,
                    includeHistory: nil,
                    generationType: .image,
                    token: state.hCaptchaToken,
                    isRemix: state.isRemix
                )

                return .merge(
                    .run { send in
                        await send(.generationResponse(prompt, Result(catching: {
                            try await api.generateV2(prompt)
                        })))
                    },
                    .send(.image(.setIsCreating(true)))
                )

            case let .generationResponse(prompt, result):
                // Let parent handle loading state and manage
                // hCaptcha token errors
                var isSubmitting = false
                if case .failure(let error) = result,
                   let apiError = error.underlyingApiError,
                   apiError == .invalidHCaptchaToken
                {
                    isSubmitting = true
                }
                switch state.media {
                case .image:
                    return .merge(
                        .send(.delegate(.generationResponse(prompt, result))),
                        .send(.image(.setIsCreating(isSubmitting)))
                    )

                case .video:
                    return .none

                case .none:
                    return .send(.delegate(.generationResponse(prompt, result)))
                }

            case .delegate:
                return .none

            case .image, .video:
                return .none

            case .setFlash(let enabled):
                state.$flash.withLock { $0 = enabled }
                return .none

            case .toggleFlash:
                state.$flash.withLock { $0.toggle() }
                return .none

            case .setDidTapCapturePhoto(let enabled):
                state.didTapCapturePhoto = enabled
                return .none

            case .setDidTapRecordVideo(let enabled):
                state.didTapRecordVideo = enabled
                state.isRecording = enabled
                return .none

            case .setDidTapFlipCamera(let enabled):
                state.didTapFlipCamera = enabled
                return .none

            case .setSelectedImage(let image):
                state.selectedImage = image
                guard image != nil else {
                    return .none
                }
                return .send(.didSelectImage)

            case .setSelectedMovie(let movie):
                state.selectedMovie = movie
                guard movie != nil else {
                    return .none
                }
                return .send(.didSelectMovie)

            case .setMediaTransferProgress(let progress):
                state.mediaTransferProgress = progress ?? 0.0
                return .none

            case .takePhoto:
                return .concatenate(
                    .send(.setDidTapCapturePhoto(true)),
                    .run { send in
                        try await Task.sleep(for: .seconds(0.1))
                        await send(.setDidTapCapturePhoto(false))
                    }
                )

            case .setRecording(let isRecording):
                if isRecording {
                    return .send(.setDidTapRecordVideo(true))
                } else {
                    return .run { send in
                        try await Task.sleep(for: .seconds(0.5))
                        await send(.setDidTapRecordVideo(false))
                    }
                }

            case .flipCamera:
                return .concatenate(
                    .send(.setDidTapFlipCamera(true)),
                    .run { send in
                        try await Task.sleep(for: .seconds(0.1))
                        await send(.setDidTapFlipCamera(false))
                    }
                )

            case .presentMediaPicker:
                state.destination = .mediaPicker
                return .none

            case .dismissMediaPicker:
                state.destination = nil
                return .none

            case .mediaPickerDidError:
                state.destination = nil
                return .none

            case .presentVideoTrimmer(let videoURL):
                state.destination = .videoTrimmer(.init(videoURL: videoURL, contentType: .scene))
                return .none

            case .dismissVideoTrimmer:
                state.destination = nil
                return .none

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

        Analytics()
    }
}

public struct CreateClipCameraView: View {
    @Namespace private var namespace
    @Bindable var store: StoreOf<CreateClipCamera>

    public init(store: StoreOf<CreateClipCamera>) {
        self.store = store
    }

    public var body: some View {
        VStack {
            switch store.media {
            case nil:
                CreateClipCameraPendingView(namespace: namespace, store: store)
                    .transition(.scale(scale: 1))
                    .disabled(store.isShowingInteractionBlock)

            case .image:
                if let store = store.scope(state: \.media?.image, action: \.image) {
                    CreateClipCameraImageView(namespace: namespace, store: store)
                        .transition(.scale(scale: 1))
                }

            case .video:
                if let store = store.scope(state: \.media?.video, action: \.video) {
                    CreateClipCameraVideoView(namespace: namespace, store: store)
                        .transition(.scale(scale: 1))
                }
            }
        }
        .overlay {
            if store.isShowingInteractionBlock {
                Color.black.opacity(store.isShowingInteractionBlock ? 0.4 : .zero)
                ProgressView()
                    .progressViewStyle(.circular)
            }
        }
        .sheet(item: $store.scope(state: \.destination?.mediaPicker, action: \.destination.mediaPicker)) { _ in
            MediaPicker(
                selectedImage: $store.selectedImage.sending(\.setSelectedImage),
                selectedMovie: $store.selectedMovie.sending(\.setSelectedMovie),
                transferProgress: $store.mediaTransferProgress.sending(\.setMediaTransferProgress),
                onSelectionComplete: {
                    if let image = store.selectedImage {
                        store.send(.importedImage(image))
                    } else if let movie = store.selectedMovie {
                        store.send(.importedVideo(movie.url))
                    }
                },
                onSelectionCancelled: {
                    store.send(.dismissMediaPicker)
                },
                onError: { _ in
                    store.send(.mediaPickerDidError)
                }
            )
        }
        .sheet(item: $store.scope(state: \.destination?.videoTrimmer, action: \.destination.videoTrimmer)) { store in
            VideoTrimmerView(store: store)
        }
    }
}

struct CreateClipCameraPendingView: View {
    @State private var animateAppear = false

    var namespace: Namespace.ID
    @Bindable var store: StoreOf<CreateClipCamera>

    var body: some View {
        CreateCameraV2(
            onPhotoCapture: { photoURL in
                Task { @MainActor in
                    guard let uiImage = UIImage(contentsOfFile: photoURL.path()) else { return }
                    store.send(.tookImage(uiImage), animation: .default)
                }
            },
            onVideoCapture: { videoURL, videoFPS in
                Task { @MainActor in
                    store.send(.tookVideo(videoURL, videoFPS), animation: .default)
                }
            },
            onMediaPickerTap: {
                store.send(.presentMediaPicker)
            }
        )
        .onAppear {
            animateAppear = true
            store.send(.stopAudioIfPlaying)
        }
    }

    var libraryButton: some View {
        Button(
            action: { store.send(.presentMediaPicker) },
            label: {
                Image.Icon.gallery
                    .foregroundStyle(Color.SemanticV1.iconBrand)
            }
        )
    }

    var shutterButton: some View {
        let longPressGestureDelay = DragGesture(minimumDistance: 0)
            .onEnded { _ in store.send(.setRecording(false)) }

        let shortPressGesture = LongPressGesture(minimumDuration: 0)
            .onEnded { _ in store.send(.takePhoto) }

        let longTapGesture = LongPressGesture(minimumDuration: 0.25)
            .onEnded { _ in store.send(.setRecording(true)) }

        let tapBeforeLongGestures = longTapGesture
            .sequenced(before: longPressGestureDelay)
            .exclusively(before: shortPressGesture)

        return Shutter()
            .gesture(tapBeforeLongGestures)
    }
}
