import AVFoundation
import Combine
import ComponentLibrary
import ComposableArchitecture
import Localization
import SwiftUI
import Utilities

// swiftlint:disable file_length

@Reducer
public struct VideoTrimmer {
    public enum ContentType: Equatable {
        case videoCover
        case scene
        case custom(caption: String, maximumDuration: Double)

        public var maximumDuration: Double {
            switch self {
            case .videoCover:
                return 10.0
            case .scene:
                return 30.0
            case .custom(_, let maximumDuration):
                return maximumDuration
            }
        }

        public var caption: String {
            switch self {
            case .videoCover:
                return L10n.FeatureVideoTrimmer.coversMessage
            case .scene:
                return L10n.FeatureVideoTrimmer.scenesMessage
            case .custom(let caption, _):
                return caption
            }
        }
    }

    @ObservableState
    public struct State: Equatable {
        public var videoURL: URL
        // Used to determine max duration and navBarSubtitle
        // if we're trimming a Scene or a video cover
        public var contentType: ContentType
        public var asset: AVAsset?
        public var player: AVPlayer?
        public var showPlayer: Bool = false

        public var leftHandlePosition: Double = 0
        public var rightHandlePosition: Double = 1

        public var cursorOffset: CGFloat = 0
        public var currentTime: Double = 0
        public var videoDuration: Double = 0

        public var thumbnails: [UIImage] = []
        public var isDragging: Bool = false
        public var isSelectionDragging: Bool = false
        public var isPlaying: Bool = false

        public let thumbnailCount = 12
        public let bottomStripWidth = UIScreen.main.bounds.width - 64
        public let minimumDuration: Double = 2.0
        public var maximumDuration: Double {
            contentType.maximumDuration
        }

        // Shows "{X} must be {Y} seconds or less" depending on content type
        public var navBarSubtitle: String {
            return contentType.caption
        }

        public var showThumbnailStrip: Bool = false
        public var isExporting: Bool = false

        public let isMuted = true

        public init(
            videoURL: URL,
            contentType: ContentType? = nil,
            maximumDuration: Double? = nil,
            caption: String? = nil
        ) {
            self.videoURL = videoURL
            if let contentType {
                self.contentType = contentType
            } else {
                // If ContentType is not provided, fallback to custom type
                let maximumDuration = maximumDuration ?? 30.0 // Fallback to 30 seconds for custom types
                let caption = caption ?? L10n.FeatureVideoTrimmer.fallbackMessage(maximumDuration)
                self.contentType = .custom(caption: caption, maximumDuration: maximumDuration)
            }
        }

        public var startTimeSeconds: Double {
            leftHandlePosition * videoDuration
        }

        public var endTimeSeconds: Double {
            rightHandlePosition * videoDuration
        }

        public var trimmedDuration: Double {
            endTimeSeconds - startTimeSeconds
        }

        public var videoDurationIsValid: Bool {
            // Adds a small buffer to the maximum duration check
            // to avoid issues with floating point precision
            let maxDurationWithBuffer = maximumDuration + 0.2

            return trimmedDuration >= minimumDuration && trimmedDuration <= maxDurationWithBuffer
        }

        public var outputURL: URL? {
            do {
                let cachesDirectory = try FileManager.default.url(
                    for: .cachesDirectory,
                    in: .userDomainMask,
                    appropriateFor: nil,
                    create: false
                )
                let outputURL = cachesDirectory.appendingPathComponent("trimmedVideo").appendingPathExtension("mp4")
                try? FileManager.default.removeItem(at: outputURL)
                return outputURL
            } catch {
                return nil
            }
        }

        public var videoPreviewOpacity: CGFloat {
            if isExporting {
                return 0.5
            } else if showPlayer {
                return 1
            } else {
                return 0
            }
        }
    }

    public enum Action: BindableAction {
        case binding(BindingAction<State>)
        case loadVideo
        case updateCursorPosition(Double)
        case seek(Double)
        case togglePlayPause
        case updateTime(CMTime)
        case `internal`(Internal)
        case delegate(Delegate)
        case dismiss
        case save
        case handleDragEnd
        case dragSelection(startOffset: Double)

        public enum Internal {
            case videoLoaded(AVAsset, AVPlayer, CMTime)
            case thumbnailsGenerated([UIImage])
            case showThumbnailStrip
        }

        public enum Delegate {
            case saveResponse(Result<URL, Error>)
            case showDurationError
        }
    }

    @Dependency(VideoPreviewClient.self) var previewClient
    @Dependency(\.mainQueue) var mainQueue
    @Dependency(\.dismiss) var dismiss

    public init() {}

    public var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .loadVideo:
                return .run { [
                    videoURL = state.videoURL,
                    thumbnailCount = state.thumbnailCount,
                    minimumDuration = state.minimumDuration,
                    isMuted = state.isMuted
                ] send in

                    let (asset, player) = try await previewClient.loadVideo(videoURL, isMuted, nil)
                    let duration = try await asset.load(.duration)
                    if duration.seconds < minimumDuration {
                        await send(.delegate(.showDurationError))
                    } else {
                        await send(.internal(.videoLoaded(asset, player, duration)))
                        let thumbnails = try await previewClient.generateThumbnails(asset, thumbnailCount)
                        await send(.internal(.thumbnailsGenerated(thumbnails)))
                    }
                }

            case let .internal(.videoLoaded(asset, player, duration)):
                state.videoDuration = duration.seconds
                state.player = player
                state.asset = asset
                state.showPlayer = true

                if state.videoDuration > state.maximumDuration {
                    state.rightHandlePosition = state.maximumDuration / state.videoDuration
                }

                return .publisher {
                    previewClient.periodicTime()
                        .map(Action.updateTime)
                }

            case let .internal(.thumbnailsGenerated(thumbnails)):
                state.thumbnails = thumbnails
                return .run { send in
                    try await Task.sleep(for: .seconds(0.1))
                    await send(.internal(.showThumbnailStrip))
                }

            case .internal(.showThumbnailStrip):
                state.showThumbnailStrip = true
                return .none

            case let .updateCursorPosition(position):
                let constrainedPosition = min(max(position, state.leftHandlePosition), state.rightHandlePosition)
                state.isDragging = true
                state.cursorOffset = CGFloat(constrainedPosition) * state.bottomStripWidth
                state.currentTime = constrainedPosition * state.videoDuration
                return .send(.seek(state.currentTime))

            case let .seek(time):
                previewClient.seek(CMTime(seconds: time, preferredTimescale: 600))
                return .none

            case .togglePlayPause:
                if state.currentTime >= state.endTimeSeconds && !state.isPlaying {
                    return .send(.updateCursorPosition(state.leftHandlePosition)).concatenate(with: .send(.handleDragEnd))
                }
                state.isPlaying.toggle()
                if state.isPlaying {
                    previewClient.play()
                } else {
                    previewClient.pause()
                }
                return .none

            case let .updateTime(time):
                let newCurrentTime = CMTimeGetSeconds(time)
                if !state.isDragging {
                    state.currentTime = newCurrentTime
                    let trimmedTime = newCurrentTime - state.startTimeSeconds
                    let progress = trimmedTime / state.trimmedDuration
                    let distanceBetweenHandles = state.rightHandlePosition - state.leftHandlePosition
                    state.cursorOffset = CGFloat(state.leftHandlePosition + progress * distanceBetweenHandles) * state.bottomStripWidth

                    if newCurrentTime >= state.endTimeSeconds {
                        return .merge(
                            .send(.seek(state.startTimeSeconds)),
                            .send(.binding(.set(\.isPlaying, false))),
                            .run { _ in
                                previewClient.pause()
                            }
                        )
                    }
                }
                return .none

            case .save:
                guard !state.isExporting else { return .none }
                state.isExporting = true
                let trimmedStart = CMTime(seconds: state.startTimeSeconds, preferredTimescale: 600)
                let trimmedEnd = CMTime(seconds: state.endTimeSeconds, preferredTimescale: 600)
                let outputURL = state.outputURL
                guard let asset = state.asset,
                      let outputURL = outputURL
                else {
                    return .none
                }
                return .run { send in
                    do {
                        let trimmedURL = try await asset.trimmedForPreview(to: outputURL, startTime: trimmedStart, endTime: trimmedEnd)
                        await send(.delegate(.saveResponse(.success(trimmedURL))))
                    } catch {
                        await send(.delegate(.saveResponse(.failure(error))))
                    }
                }

            case .binding(\.leftHandlePosition):
                if state.currentTime < state.startTimeSeconds {
                    return .send(.seek(state.startTimeSeconds))
                }
                return .none

            case .binding(\.rightHandlePosition):
                if state.currentTime > state.endTimeSeconds {
                    return .send(.seek(state.startTimeSeconds))
                }
                return .none

            case .binding(\.isPlaying):
                if state.isPlaying {
                    previewClient.play()
                } else {
                    previewClient.pause()
                }
                return .none

            case let .dragSelection(startOffset):
                let selectionWidth = state.rightHandlePosition - state.leftHandlePosition

                // Calculate new positions based on the direct offset
                var newLeftPosition = startOffset
                var newRightPosition = newLeftPosition + selectionWidth

                // Constrain the entire selection within bounds
                if newLeftPosition < 0 {
                    newLeftPosition = 0
                    newRightPosition = selectionWidth
                }

                if newRightPosition > 1 {
                    newRightPosition = 1
                    newLeftPosition = 1 - selectionWidth
                }

                // Update positions
                state.leftHandlePosition = newLeftPosition
                state.rightHandlePosition = newRightPosition
                state.isSelectionDragging = true

                // Always position the cursor at the start of the selection
                // This gives a preview of the first frame as the user drags
                state.cursorOffset = CGFloat(newLeftPosition) * state.bottomStripWidth
                state.currentTime = newLeftPosition * state.videoDuration

                return .send(.seek(state.currentTime))

            case .handleDragEnd:
                state.isDragging = false
                state.isSelectionDragging = false
                return .none

            case .dismiss:
                return .run { _ in
                    previewClient.teardown()
                    await dismiss()
                }

            case .binding, .delegate:
                return .none
            }
        }

        Analytics()
    }
}

public struct VideoTrimmerView: View {
    @Bindable var store: StoreOf<VideoTrimmer>

    let aspectRatio = 9.0 / 16.0
    let previewHeight: CGFloat = UIScreen.isCompact ? 408.0 : 528.0

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

    public var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                navBar
                videoPreview
                ZStack(alignment: .leading) {
                    thumbnailStrip
                    handles
                    playbackCursor
                }
                .frame(height: 70)
                .padding(.horizontal, 32)
                .opacity(store.showThumbnailStrip ? 1 : 0)
                Spacer()
            }
            .onAppear {
                store.send(.loadVideo)
            }
            .background(Color.SemanticV1.backgroundPrimary)
        }
        .animation(.easeInOut(duration: 0.2), value: store.showThumbnailStrip)
    }

    @ViewBuilder
    var videoPreview: some View {
        ZStack {
            Color.SemanticV1.alwaysBlack1
            VideoPreviewView(player: store.player, resizeMode: .resizeAspect)
                .aspectRatio(aspectRatio, contentMode: .fill)
                .opacity(store.videoPreviewOpacity)
                .overlay {
                    mediaActionBar
                        .opacity(store.videoPreviewOpacity)
                    if store.isExporting {
                        ProgressView()
                    }
                }
        }
        .frame(height: previewHeight)
        .clipShape(RoundedRectangle(cornerRadius: 20))
        .padding(.top, 18)
        .padding(.bottom, 34)
        .padding(.horizontal, 48)
    }

    @ViewBuilder
    var handles: some View {
        TrimmerHandles(
            start: $store.leftHandlePosition,
            end: $store.rightHandlePosition,
            totalWidth: store.bottomStripWidth,
            duration: store.videoDuration,
            minimumDuration: store.minimumDuration,
            maximumDuration: store.maximumDuration,
            onLeftHandleDrag: { position in
                store.send(.binding(.set(\.isPlaying, false)))
                store.send(.updateCursorPosition(position))
            },
            onRightHandleDrag: { position in
                store.send(.binding(.set(\.isPlaying, false)))
                store.send(.updateCursorPosition(position))
            },
            onLeftHandleDragEnd: { _ in
                store.send(.handleDragEnd)
            },
            onRightHandleDragEnd: { _ in
                store.send(.handleDragEnd)
            },
            onScrub: { _ in },
            onSelectionDrag: { startOffset in
                store.send(.binding(.set(\.isPlaying, false)))
                store.send(.dragSelection(startOffset: startOffset))
            },
            onSelectionDragEnd: {
                store.send(.handleDragEnd)
            }
        )
        .frame(height: 60)
    }

    @ViewBuilder
    var playbackCursor: some View {
        PlaybackCursor(
            start: $store.leftHandlePosition,
            end: $store.rightHandlePosition,
            totalWidth: store.bottomStripWidth,
            offset: store.cursorOffset,
            height: 70,
            onScrub: { position in
                store.send(.updateCursorPosition(position))
            },
            onScrubEnd: {
                store.send(.handleDragEnd)
            }
        )
    }

    @ViewBuilder
    var thumbnailStrip: some View {
        ThumbnailStrip(
            thumbnails: store.thumbnails,
            width: store.bottomStripWidth,
            start: store.leftHandlePosition,
            end: store.rightHandlePosition
        )
    }

    @ViewBuilder
    var mediaActionBar: some View {
        VStack {
            Spacer()
            HStack {
                PlayPauseButton(isPlaying: store.isPlaying) {
                    store.send(.togglePlayPause)
                }
                Spacer()
                TimeElapsedView(currentTime: store.currentTime,
                                trimStart: store.leftHandlePosition,
                                trimEnd: store.rightHandlePosition,
                                totalDuration: store.videoDuration)
            }
            .padding(.leading, 6)
            .padding(.trailing, 12)
            .padding(.bottom, 12)
        }
        .opacity(store.showPlayer ? 1 : 0)
        .animation(.easeInOut(duration: 0.2), value: store.showPlayer)
    }

    @ViewBuilder
    var navBar: some View {
        // This design doesn't really conform to our existing
        // `sheetNavigationBar` or other `.toolbar` implementations,
        // so I defined a custom implementation here.
        ZStack(alignment: .top) {
            VStack(spacing: 8) {
                ToolbarTitle(L10n.FeatureVideoTrimmer.trimVideo)
                Text(store.navBarSubtitle)
                    .typographyV1(.caption2.lineHeight(24.0))
                    .foregroundStyle(Color.SemanticV1.textSecondary)
            }
            .frame(maxWidth: .infinity, alignment: .center)
            HStack {
                Button(L10n.FeatureVideoTrimmer.cancel) {
                    store.send(.dismiss)
                }
                .typographyV1(.body3)
                .foregroundStyle(Color.SemanticV1.textSecondary)
                .buttonStyle(.plain)

                Spacer()

                Button(L10n.FeatureVideoTrimmer.save) {
                    store.send(.save)
                }
                .typographyV1(.body3)
                .foregroundStyle(Color.SemanticV1.textPrimary)
                .buttonStyle(.plain)
                .disabled(!store.videoDurationIsValid)
                .opacity(store.videoDurationIsValid ? 1 : 0.5)
            }
        }
        .frame(height: 56)
        .padding(.horizontal, 20)
        .padding(.top, 20)
    }
}
