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

// swiftlint:disable file_length

@Reducer
public struct VideoTrimmerV2 {
    @Reducer(state: .equatable)
    public enum Destination {
        case alert(AlertState<Alert>)
        public enum Alert {
            case discardChanges
        }
    }

    public enum ContentType: Equatable {
        case videoCover
        case scene
        case hook
        case custom(caption: String, maximumDuration: Double)

        public var maximumDuration: Double {
            switch self {
            case .videoCover:
                return 10.0
            case .scene:
                return 30.0
            case .hook:
                return HooksConstants.maxHookMediaDuration
            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 .hook:
                return "Trim Video"
            case .custom(let caption, _):
                return caption
            }
        }
    }

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

        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
        public var rightHandlePosition: Double

        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 var minimumDuration: Double
        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,
            startTime: Double? = nil,
            endTime: Double? = nil,
            duration: Double? = nil,
            contentType: ContentType? = nil,
            maximumDuration: Double? = nil,
            minimumDuration: Double = 2.0,
            caption: String? = nil
        ) {
            self.videoURL = videoURL
            self.minimumDuration = minimumDuration
            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)
            }
            if let startTime, let endTime, let duration {
                self.leftHandlePosition = startTime / duration
                self.rightHandlePosition = endTime / duration
            } else {
                self.leftHandlePosition = 0
                self.rightHandlePosition = 1
            }
        }

        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 destination(PresentationAction<Destination.Action>)
        case binding(BindingAction<State>)
        case loadVideo
        case updateCursorPosition(Double)
        case seek(Double)
        case togglePlayPause
        case updateTime(CMTime)
        case `internal`(Internal)
        case delegate(Delegate)
        case dismissTapped
        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, Double, Double), 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 { [startTime = state.startTimeSeconds, endTime = state.endTimeSeconds] send in
                    do {
                        let trimmedURL = try await asset.trimmedForPreview(to: outputURL, startTime: trimmedStart, endTime: trimmedEnd)
                        await send(.delegate(.saveResponse(.success((trimmedURL, startTime, endTime)))))
                    } catch {
                        await send(.delegate(.saveResponse(.failure(error))))
                    }
                }

            case .binding(\.leftHandlePosition):
                if state.currentTime < state.startTimeSeconds {
                    state.currentTime = state.startTimeSeconds
                    state.cursorOffset = CGFloat(state.leftHandlePosition) * state.bottomStripWidth
                    return .send(.seek(state.currentTime))
                }
                return .none

            case .binding(\.rightHandlePosition):
                if state.currentTime > state.endTimeSeconds {
                    state.currentTime = state.endTimeSeconds
                    state.cursorOffset = CGFloat(state.rightHandlePosition) * state.bottomStripWidth
                    return .send(.seek(state.currentTime))
                }
                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 .dismissTapped:
                guard true else {
                    return .send(.dismiss)
                }

                // User has unsaved changes, show alert
                state.destination = .alert(.init(
                    title: { TextState(L10n.FeatureManageClip.discardChangesAlertTitle) },
                    actions: {
                        ButtonState(role: .destructive, action: .send(.discardChanges)) { TextState(L10n.FeatureManageClip.discardChangesAlertConfirm) }
                        ButtonState(role: .cancel) { TextState(L10n.FeatureManageClip.discardChangesAlertCancel) }
                    },
                    message: { TextState(L10n.FeatureManageClip.discardChangesAlertMessage) }
                ))
                return .none

            case .destination(.presented(.alert(.discardChanges))):
                state.destination = nil
                return .send(.dismiss)

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

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

//        Analytics()
    }
}

public struct VideoTrimmerViewV2: View {
    @Bindable var store: StoreOf<VideoTrimmerV2>

    let previewHeight: CGFloat = UIScreen.isCompact ? 408.0 : 610.0

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

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

    @ViewBuilder
    var videoPreview: some View {
        VideoPreviewView(player: store.player, resizeMode: .resizeAspect)
            .aspectRatio(contentMode: .fill)
            .opacity(store.videoPreviewOpacity)
            .contentShape(Rectangle())
            .onTapGesture {
                store.send(.togglePlayPause)
            }
            .overlay {
                durationPill
                    .opacity(store.videoPreviewOpacity)
                if store.isExporting {
                    ProgressView()
                }
            }
            .frame(width: 310, height: 610)
            .background(Color.SemanticV1.alwaysBlack1)
            .clipShape(RoundedRectangle(cornerRadius: 20))
            .overlay {
                RoundedRectangle(cornerRadius: 20)
                    .strokeBorder(Color.white.opacity(0.3), lineWidth: 0.5)
            }
            .padding(.bottom, 21)
            .padding(.top, 12)
    }

    @ViewBuilder
    var handles: some View {
        TrimmerHandles(
            start: $store.leftHandlePosition,
            end: $store.rightHandlePosition,
            totalWidth: store.bottomStripWidth,
            duration: store.videoDuration,
            minimumDuration: store.minimumDuration,
            maximumDuration: store.maximumDuration,
            accentColor: Color.white,
            showHandleTimestamps: false,
            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 durationPill: some View {
        if store.isDragging {
            timeElapsedView
                .transition(.scale(scale: 0.9).combined(with: .opacity))
                .animation(.easeInOut(duration: 0.3), value: store.isDragging)
                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
                .padding(.bottom, 20)
        }
    }

    @ViewBuilder
    var navBar: some View {
        ZStack(alignment: .center) {
            navigationTitle
            HStack {
                dismissButton
                Spacer()
                saveButton
            }
        }
        .frame(height: 64)
        .padding(.horizontal, 12)
    }

    @ToolbarContentBuilder
    private var toolbarContent: some ToolbarContent {
        ToolbarItem(placement: .topBarLeading) {
            dismissButton
        }
        ToolbarItem(placement: .principal) {
            navigationTitle
        }
        ToolbarItem(placement: .topBarTrailing) {
            saveButton
        }
    }

    @ViewBuilder
    private var dismissButton: some View {
        ToolbarButton(.close, color: Color.SemanticV2.foregroundPrimary, background: Color.SemanticV1.backgroundQuaternary) {
            store.send(.dismissTapped)
        }
    }

    @ViewBuilder
    private var navigationTitle: some View {
        Text("Trim Video")
            .typographyV1(.headline4.size { _ in 16.0 }.lineHeight(22.0))
            .foregroundStyle(Color.SemanticV2.foregroundPrimary)
    }

    @ViewBuilder
    private var saveButton: some View {
        let color = Color.SemanticV2.backgroundPrimary
        ToolbarButton(
            .checkmark,
            color: store.isExporting ? .clear : color,
            background: Color.SemanticV2.foregroundPrimary
        ) {
            guard !store.isExporting else { return }
            store.send(.save)
        }
        .overlay {
            ProgressView()
                .opacity(store.isExporting ? 1 : 0)
                .frame(width: 24, height: 24)
                .tint(Color.SemanticV2.backgroundPrimary)
        }
    }

    private var hasUnsavedChanges: Bool {
        return true
    }

    private var formattedDuration: String {
        let total = (store.rightHandlePosition - store.leftHandlePosition) * store.videoDuration
        return "\(Int(total))s"
    }

    @ViewBuilder
    private var timeElapsedView: some View {
        Text(formattedDuration)
            .foregroundColor(.white)
            .typographyV1(.monospace.size { _ in 14.0 }.lineHeight(22.0))
            .padding(.horizontal, 22)
            .padding(.vertical, 10)
            .background {
                Capsule()
                    .fill(.ultraThinMaterial)
                    .strokeBorder(Color.white.opacity(0.1), lineWidth: 0.5)
            }
    }
}
