import BackendEnvironmentClient
import ComposableArchitecture
import Foundation
import GenAPI

public struct Clip: Identifiable, Equatable, Hashable {
    /** Abstraction for app only */
    public enum HighLevelModelVersion {
        case previousToV4
        case v4
        case v4_5
        case v4_5Plus
        case v5
    }

    public enum ErrorType: String, Codable {
        case moderation = "moderation_failure"
        case undefined
    }

    public enum ClipType: String, Codable {
        case gen
        case preview
    }

    public enum Status: String, Codable {
        case submitted
        case queued
        case streaming
        case complete
        case error
        case undefined
    }

    public var id: ClipID
    public let audioUrl: String
    public var title: String
    public var prompt: String
    public let modelName: String
    public let majorModelVersion: String
    public let gptDescriptionPrompt: String
    public var imageUrl: String
    public var largeImageUrl: String
    public let tags: String
    public var displayTags: String?
    public var playCount: Int
    public var upvoteCount: Int
    public var isPublic: Bool
    public var duration: Double
    public var hasVocal: Bool
    public var canPublishWithVocal: Bool
    public let status: Status
    public let displayName: String
    public let userId: User.ID
    public let handle: String
    public var commentCount: Int
    public var avatarImageUrl: String?
    public var isTrashed: Bool
    public let createdAt: Date?
    public var errorType: ErrorType?
    public var errorMessage: String?
    public var reactionType: String?
    public var coverClipId: String?
    public var canRemix: Bool
    public var isRemix: Bool
    public var showRemix: Bool
    public var caption: String?
    public var captionMentions: [CaptionUserMention]
    public var isPinned: Bool
    public var videoCoverUrl: String?
    public var videoCoverPreviewUrl: String?
    public var negativeTags: String?
    public var weirdnessConstraint: Double?
    public var styleWeight: Double?
    public var hasHook: Bool = false
    public var hookPreviewThumbnailUrl: String? = nil
    public var isAudioUpload: Bool = false
    public var optOutVideoCoverHook: Bool = false
    public var modelBadgeStyle: ModelBadgeStyle? = nil
    public var type: ClipType = .gen
    // TODO: Remove after scenes is completely deprecated
    public var isScene: Bool
    public var videoToSongVideoOutputUrl: String?
    public var isImageToSong: Bool

    // Generic dimensions used for video covers, video uploads for scenes, etc.
    public var videoUploadWidth: Int?
    public var videoUploadHeight: Int?

    // Task used to generate the clip - Extend, Infill, etc.
    // This is undefined for new generations
    public var task: Prompt.TaskType?

    // Ancestry of the clip if it isn't a new generation
    public var history: ClipHistory?

    // Concat history of the clip if it is a product of a concat operation
    public var concatHistory: ConcatHistory?

    public var isLiked: Bool {
        get { reactionType == "L" }
        set {
            reactionType = newValue ? "L" : nil
            localUpvoteModifier = reactionType == "L" ? min(1, localUpvoteModifier + 1) : max(-1, localUpvoteModifier - 1)
        }
    }

    public var isDisliked: Bool {
        get { reactionType == "D" }
        set {
            localUpvoteModifier = reactionType == "L" ? max(-1, localUpvoteModifier - 1) : localUpvoteModifier
            reactionType = newValue ? "D" : nil
        }
    }

    private var localUpvoteModifier: Int = 0

    public var localUpvoteCount: Int {
        upvoteCount + localUpvoteModifier
    }

    public var highLevelModel: HighLevelModelVersion {
        if modelName.contains("chirp-crow") {
            return .v5
        } else if modelName.contains("bluejay") {
            return .v4_5Plus
        } else if modelName.contains("auk") || modelName.contains("ahi") {
            return .v4_5
        } else if modelName.contains("v4") {
            return .v4
        } else {
            return .previousToV4
        }
    }

    /// Only use this when the request to get a attributable share URL fails
    public var fallbackShareURL: URL {
        let baseUrl = BackendEnvironmentProvider.currentConfiguration().webEndpointHost
        return URL(string: "https://\(baseUrl)/song/\(id.remoteId)")!
    }

    public var isInstrumental: Bool {
        prompt.lowercased()
            .split(separator: "[instrumental]")
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .allSatisfy { $0 == "" }
    }

    public var isRootClip: Bool {
        return history?.rootClip == id
    }

    public var isFullSongFromEdits: Bool {
        guard let concatHistory, !concatHistory.clips.isEmpty else { return false }
        return true
    }

    // TODO: Make this generic once we can determine the orientation of images
    public var isVideoLandscape: Bool {
        guard let videoUploadWidth, let videoUploadHeight else { return false }
        return videoUploadWidth > videoUploadHeight
    }

    // Should disable client-side downloads
    public enum DownloadDisabledReason {
        case remixContest, other
    }

    public var downloadDisabledReason: DownloadDisabledReason?
}

public extension Clip {
    init(
        clipId: ClipID,
        audioUrl: String,
        title: String,
        prompt: String,
        modelName: String,
        majorModelVersion: String,
        gptDescriptionPrompt: String,
        imageUrl: String,
        largeImageUrl: String,
        tags: String,
        displayTags: String?,
        playCount: Int,
        upvoteCount: Int,
        isPublic: Bool,
        duration: Double,
        hasVocal: Bool,
        canPublishWithVocal: Bool,
        status: Clip.Status,
        displayName: String,
        userId: User.ID,
        handle: String,
        commentCount: Int = 0,
        avatarImageUrl: String? = nil,
        isTrashed: Bool,
        createdAt: Date? = nil,
        errorType: Clip.ErrorType? = nil,
        errorMessage: String? = nil,
        reactionType: String? = nil,
        localUpvoteModifier: Int = 0,
        isImageToSong: Bool = false,
        isScene: Bool = false,
        videoToSongVideoOutputUrl: String? = nil,
        task: Prompt.TaskType? = nil,
        history: ClipHistory? = nil,
        concatHistory: ConcatHistory? = nil,
        coverClipId: String? = nil,
        canRemix: Bool = false,
        isRemix: Bool = false,
        showRemix: Bool = true,
        caption: String? = nil,
        captionMentions: [CaptionUserMention] = [],
        isPinned: Bool,
        videoCoverUrl: String? = nil,
        videoCoverPreviewUrl: String? = nil,
        downloadDisabledReason: DownloadDisabledReason? = nil,
        negativeTags: String? = nil,
        weirdnessConstraint: Double? = 0,
        styleWeight: Double? = 0,
        optOutVideoCoverHook: Bool = false,
        modelBadgeStyle: ModelBadgeStyle? = nil,
        type: ClipType = .gen
    ) {
        self.id = clipId
        self.audioUrl = audioUrl
        self.title = title
        self.prompt = prompt
        self.modelName = modelName
        self.majorModelVersion = majorModelVersion
        self.gptDescriptionPrompt = gptDescriptionPrompt
        self.imageUrl = imageUrl
        self.largeImageUrl = largeImageUrl
        self.tags = tags
        self.displayTags = displayTags
        self.playCount = playCount
        self.upvoteCount = upvoteCount
        self.isPublic = isPublic
        self.duration = duration
        self.hasVocal = hasVocal
        self.canPublishWithVocal = canPublishWithVocal
        self.status = status
        self.displayName = displayName
        self.userId = userId
        self.handle = handle
        self.commentCount = commentCount
        self.avatarImageUrl = avatarImageUrl
        self.isTrashed = isTrashed
        self.createdAt = createdAt
        self.errorType = errorType
        self.errorMessage = errorMessage
        self.reactionType = reactionType
        self.localUpvoteModifier = localUpvoteModifier
        self.task = task
        self.history = history
        self.concatHistory = concatHistory
        self.coverClipId = coverClipId
        self.canRemix = canRemix
        self.isRemix = isRemix
        self.showRemix = showRemix
        self.caption = caption
        self.captionMentions = captionMentions
        self.isPinned = isPinned
        self.videoCoverUrl = videoCoverUrl
        self.videoCoverPreviewUrl = videoCoverPreviewUrl
        self.downloadDisabledReason = downloadDisabledReason
        self.negativeTags = negativeTags
        self.weirdnessConstraint = weirdnessConstraint
        self.styleWeight = styleWeight
        self.optOutVideoCoverHook = optOutVideoCoverHook
        self.modelBadgeStyle = modelBadgeStyle
        self.type = type
        self.isScene = isScene
        self.videoToSongVideoOutputUrl = videoToSongVideoOutputUrl
        self.isImageToSong = isImageToSong
    }
}

extension Clip {
    init(_ remote: GenAPI.PlaylistClipSchema) throws {
        try self.init(remote.clip)
        id = try .init(remote)
    }

    public init(_ remote: GenAPI.GeneratedClipSchema) throws {
        id = try .init(remote)
        audioUrl = remote.audioUrl ?? ""
        title = {
            guard let title = remote.title?.trimmingCharacters(in: .whitespacesAndNewlines),
                  !title.isEmpty
            else { return "Untitled" }
            let singleLineTitle = title.replacingOccurrences(of: "\n", with: " ")
            return singleLineTitle
        }()
        prompt = remote.metadata.prompt ?? ""
        modelName = remote.modelName
        majorModelVersion = remote.majorModelVersion
        gptDescriptionPrompt = remote.metadata.gptDescriptionPrompt ?? ""
        imageUrl = remote.imageUrl ?? ""
        largeImageUrl = remote.imageLargeUrl ?? ""
        tags = remote.metadata.tags?
            .trimmingCharacters(in: .whitespaces)
            .replacingOccurrences(of: "  ", with: " ") ?? ""
        displayTags = remote.displayTags?
            .trimmingCharacters(in: .whitespaces)
            .replacingOccurrences(of: "  ", with: " ") ?? ""
        playCount = remote.playCount ?? 0
        upvoteCount = remote.upvoteCount ?? 0
        isPublic = remote.isPublic ?? false
        duration = remote.metadata.duration ?? 0
        hasVocal = remote.metadata.hasVocal ?? false
        canPublishWithVocal = remote.metadata.canPublishWithVocal ?? false
        status = {
            guard let string = remote.status else { return .undefined }
            return .init(rawValue: string) ?? .undefined
        }()
        displayName = remote.displayName ?? ""
        userId = remote.userId ?? ""
        handle = remote.handle ?? ""
        commentCount = remote.commentCount ?? .zero
        avatarImageUrl = remote.avatarImageUrl
        isTrashed = remote.isTrashed
        createdAt = remote.createdAt
        errorType = .init(rawValue: remote.metadata.errorType ?? "")
        errorMessage = remote.metadata.errorMessage
        reactionType = remote.reaction?.reactionType
        task = {
            guard let task = remote.metadata.task else { return nil }
            return .init(rawValue: task.rawValue)
        }()
        history = try? .init(remote)
        concatHistory = try? .init(remote)
        coverClipId = remote.metadata.coverClipId
        canRemix = remote.metadata.canRemix ?? false
        isRemix = remote.metadata.isRemix ?? false
        showRemix = remote.metadata.showRemix ?? true /// Default to `true`
        caption = remote.caption
        let schemaCaptionMentions: [Mention] = remote.captionMentions?.userMentions ?? []
        captionMentions = schemaCaptionMentions.compactMap { try? CaptionUserMention($0) }
        isPinned = remote.isPinned ?? false
        videoCoverUrl = remote.videoCoverUrl
        videoCoverPreviewUrl = remote.previewUrl
        videoUploadWidth = remote.metadata.videoUploadWidth
        videoUploadHeight = remote.metadata.videoUploadHeight
        negativeTags = remote.metadata.negativeTags
        weirdnessConstraint = remote.metadata.controlSliders?.weirdnessConstraint
        styleWeight = remote.metadata.controlSliders?.styleWeight
        hasHook = remote.hasHook
        hookPreviewThumbnailUrl = remote.hookPreviewThumbnailUrl
        isAudioUpload = remote.metadata.type == "upload"
        optOutVideoCoverHook = remote.metadata.optOutVideoCoverHook ?? false
        modelBadgeStyle = ModelBadgeStyle(songrowBadge: remote.metadata.modelBadges?.songrow)
        downloadDisabledReason = {
            return switch remote.downloadDisabledReason {
            case GeneratedClipSchema.DownloadDisabledReason.remixContest:
                .remixContest
            case nil:
                nil
            default:
                .other
            }
        }()
        type = {
            guard let type = remote.metadata.type else { return .gen }
            return .init(rawValue: type) ?? .gen
        }()
        videoToSongVideoOutputUrl = remote.metadata.videoToSongVideoOutputUrl
        isScene = remote.metadata.isSunoShort ?? false
        isImageToSong = remote.metadata.isImageToSong ?? false
    }
}

public struct ClipID: Hashable, Codable {
    public var remoteId: String

    public var global: ClipID {
        .init(remoteId: remoteId)
    }

    public init(remoteId: String) {
        self.remoteId = remoteId
    }
}

extension ClipID {
    // Make sure that remoteId is a lowercase string,
    // otherwise network requests that use the clipId
    // in the URL path would fail.
    public init(_ remote: GenAPI.GeneratedClipSchema) throws {
        remoteId = remote.id.uuidString.lowercased()
    }

    init(_ remote: GenAPI.PlaylistClipSchema) throws {
        remoteId = remote.clip.id.uuidString.lowercased()
    }
}

public extension IdentifiedArray where ID == Clip.ID {
    func index(clipId: ID) -> Int? {
        self.index(id: clipId) ?? self.index(id: clipId.global)
    }

    @discardableResult
    mutating func removeClip(id: ID) -> Element? {
        self.remove(id: id) ?? self.remove(id: id.global)
    }
}

#if DEBUG
    public extension Clip {
        static func mock(id: String = UUID().uuidString) -> Clip {
            Clip(
                id: ClipID(remoteId: id),
                audioUrl: "https://example.com/audio.mp3",
                title: "Mock Clip Title",
                prompt: "A mock prompt for testing",
                modelName: "mock-model",
                majorModelVersion: "v4",
                gptDescriptionPrompt: "Describe this mock clip",
                imageUrl: "https://example.com/image.jpg",
                largeImageUrl: "https://example.com/large-image.jpg",
                tags: "mock,test,demo",
                displayTags: "mock, test, display",
                playCount: 100,
                upvoteCount: 50,
                isPublic: true,
                duration: 30.0,
                hasVocal: true,
                canPublishWithVocal: false,
                status: .complete,
                displayName: "Mock User",
                userId: "mock-user-123",
                handle: "mockuser",
                commentCount: 10,
                isTrashed: false,
                createdAt: Date(),
                canRemix: true,
                isRemix: false,
                showRemix: true,
                caption: "Thanks to John Doe and @alice for the inspiration!",
                captionMentions: [
                    CaptionUserMention(start: 10, end: 18, handle: "johndoe"),
                    CaptionUserMention(start: 23, end: 29, handle: "alice"),
                ],
                isPinned: false,
                isScene: false,
                isImageToSong: false
            )
        }
        
        static func internalMenuPlaceholderClip() -> Clip {
            Clip(
                id: ClipID(remoteId: "1995bec5-4ebd-4b22-a0c0-22d4b59354be"),
                audioUrl: "https://cdn1.suno.ai/1995bec5-4ebd-4b22-a0c0-22d4b59354be.mp3",
                title: "Faded On The Mantel",
                prompt: "[Verse]\nA faded photo on the mantel\nA story trapped in a wooden frame\nDust collects like whispers gentle\nBut no one dares to say the name\n\n[Chorus]\nWho were you back then\nWhen the fire burned bright\nWhen the stars bowed down\nTo your reckless flight",
                modelName: "chirp-crow",
                majorModelVersion: "v5",
                gptDescriptionPrompt: "anthemic, blues, a faded photo on the mantel",
                imageUrl: "https://cdn2.suno.ai/image_1995bec5-4ebd-4b22-a0c0-22d4b59354be.jpeg",
                largeImageUrl: "https://cdn2.suno.ai/image_large_1995bec5-4ebd-4b22-a0c0-22d4b59354be.jpeg",
                tags: "anthemic, soulful piano, blues, raw guitar riffs, raspy male vocals",
                displayTags: "anthemic, soulful piano, blues, raw guitar riffs, raspy male vocals",
                playCount: 14,
                upvoteCount: 0,
                isPublic: true,
                duration: 191.08,
                hasVocal: true,
                canPublishWithVocal: true,
                status: .complete,
                displayName: "bassel not staff",
                userId: "ddb06dcf-d0aa-40e0-b179-20b0a25a4c55",
                handle: "whomenoway",
                commentCount: 0,
                avatarImageUrl: "https://cdn1.suno.ai/sAura21.jpg",
                isTrashed: false,
                createdAt: Date(),
                canRemix: false,
                isRemix: false,
                showRemix: true,
                captionMentions: [],
                isPinned: false,
                isScene: false,
                isImageToSong: false
            )
        }
    }
#endif
