import ComposableArchitecture
import Foundation
import GenAPI
import Localization

public struct BlendedCreatePrompt: Equatable, Codable {
    // MARK: GenParamsSpec Properties

    // The property names are not meant to have parity with the `GenParamsSpec`, and instead reflect a model for our creation UIs.

    // MARK: - Core

    /// The title of the song
    public var title: String = ""
    /// Simple mode description. Equivalent to `GenParamsSpec.gptDescriptionPrompt`
    public var description: String = ""
    /// Usually comma-separated style tags. Equivalent to `GenParamsSpec.tags`
    public var styleText: String = ""
    /// Usually comma-separated style tags. Equivalent to `GenParamsSpec.negativeTags`
    public var excludeStyleText: String = ""
    /// Lyrics. Equivalent to `GenParamsSpec.prompt`
    public var lyrics: String = ""
    /// Full reflection of `GenParamsSpec.generationType`. This one's a weird one as audio uploads / creation w/ Clips counts as `.text`, not `.audio`. The general rule of thumb is that if you add text, it's `.text`. Scenes are usually `.image` or `.video`.
    public var generationType: GenerationType = .text

    // MARK: - Audio

    /// Clip ID for covers. Required if `task` is `.cover`. Cannot and should not co-exist with a non-nil `continueClipId`
    public var coverClipId: String?
    /// Clip ID for extends. Required if `task` is `.uploadExtend`. Cannot and should not co-exist with a non-nil `coverClipId`
    public var continueClipId: UploadRequest.ID?
    /// The audio timestamp in which to apply the audio generation task types. Generally `0` for `.cover` and the length of the clip for `.extend`
    public var continueAt: TimeInterval?
    /// Should include clip edit history
    public var includeHistory: Bool?

    // MARK: - Additional Options

    public var lyricsModel: String? // Decode from upstream

    public static let defaultSliderValue: Double = 0.5
    public var weirdnessConstraint: Double = defaultSliderValue
    public var styleWeight: Double = defaultSliderValue
    public var audioWeight: Double = defaultSliderValue

    // MARK: UI Modeling + Hermetic Generation

    // Modeling these pieces of state here allows us to store and reuse prompt generations effectively.

    @CasePathable
    @dynamicMemberLookup
    public enum CreateMode: Codable, Identifiable {
        case simple
        case custom

        // For conformance
        public var id: Int {
            switch self {
            case .simple: return 0
            case .custom: return 1
            }
        }

        public static func from(string: String?) -> Self? {
            switch string {
            case "simple":
                return .simple
            case "custom":
                return .custom
            default:
                return nil
            }
        }

        public func toString() -> String {
            switch self {
            case .simple:
                return "simple"
            case .custom:
                return "custom"
            }
        }
    }

    public var createMode: CreateMode = .simple

    @CasePathable
    @dynamicMemberLookup
    public enum LyricsMode: CaseIterable, Codable {
        case auto, instrumental, write

        public var isInstrumental: Bool {
            return self == .instrumental
        }
    }

    public var lyricsMode: LyricsMode = .auto

    public var useNegativeTags: Bool = false // Pre-JCB sliders
    public var useAdvancedOptions: Bool = false // Post-JCB sliders

    // Audio
    public var audioRecording: AudioRecording? {
        didSet {
            // Default to custom mode for audio create
            if audioRecording != nil {
                createMode = .custom
            }
        }
    }

    public var audioUploadRequestId: String?
    public var clipId: String?

    public enum AudioCreateStyle: CaseIterable, Codable, Equatable {
        case cover, extend

        var task: Prompt.TaskType {
            switch self {
            case .cover:
                return .cover
            case .extend:
                return .extend
            }
        }
    }

    public var audioCreateStyle: AudioCreateStyle = .cover // serialize this in order to keep it between generations
    public var isAudioCreate: Bool {
        coverClipId != nil ||
            continueClipId != nil ||
            clipId != nil ||
            audioUploadRequestId != nil ||
            audioRecording != nil
    }

    // hCatpcha Token, if required
    public var token: String?

    public var task: Prompt.TaskType?

    // Remix
    public var isRemix: Bool?

    public var canCreate: Bool {
        // If we're in audio and we don't have an uploaded clip or clip ID, we can't generate
        if isAudioCreate && clipId == nil && audioUploadRequestId == nil {
            return false
        }
        // Basic generation requirements
        switch self.createMode {
        case .simple:
            return !self.description.isEmpty ||
                (self.lyricsMode == .write && !self.lyrics.isEmpty)

        case .custom:
            return !self.styleText.isEmpty ||
                (self.lyricsMode != .instrumental && !self.lyrics.isEmpty) // Temp: for now treat auto and write as the same in custom
        }
    }

    /// To model available sliders in the UI
    public enum Sliders {
        case weirdness, styleStrength, audioStrength

        public var metadataTitle: String {
            switch self {
            case .weirdness:
                return "weirdness_constraint"
            case .styleStrength:
                return "style_weight"
            case .audioStrength:
                return "audio_weight"
            }
        }
    }

    public var availableSliders: [Sliders] {
        isAudioCreate ? [.weirdness, .styleStrength, .audioStrength] : [.weirdness, .styleStrength]
    }

    public var slidersHaveChanges: Bool {
        return self.weirdnessConstraint != BlendedCreatePrompt.defaultSliderValue ||
            self.styleWeight != BlendedCreatePrompt.defaultSliderValue ||
            self.audioWeight != BlendedCreatePrompt.defaultSliderValue
    }

    public init() {}
}

// MARK: Bridge from older `Prompt` model

// Allows for reuse style and more.

public extension BlendedCreatePrompt {
    init(from prompt: Prompt) {
        self.title = prompt.title
        self.lyrics = prompt.lyrics
        // This could use more edge case testing
        self.lyricsMode = prompt.instrumental ? .instrumental : .auto
        self.styleText = prompt.styles
        self.excludeStyleText = prompt.excludeStyles
        self.description = prompt.text
        self.coverClipId = prompt.coverClipId
        self.continueClipId = prompt.continueClipId
        self.continueAt = prompt.continueAt
        self.includeHistory = prompt.includeHistory
        self.audioRecording = prompt.audioRecording
        self.generationType = prompt.generationType
        self.token = prompt.token
        self.task = prompt.task
        self.audioRecording = prompt.audioRecording // Copied from the legacy "Reuse Prompt" logic. We don't actually get this recording from upstream, so this logic was invalid in legacy and invalid right now. TODO: Pull the id from `coverClipId` or `continueClipId` and go from there.

        // Set `CreateMode` and `LyricsMode` based on Prompt values
        self.createMode = CreateMode.from(string: prompt.createMode) ?? (prompt.isCustomized ? .custom : .simple)
        self.styleWeight = prompt.styleWeight ?? BlendedCreatePrompt.defaultSliderValue
        self.weirdnessConstraint = prompt.weirdnessConstraint ?? BlendedCreatePrompt.defaultSliderValue
        self.lyricsModel = prompt.lyricsModel
    }

    /// For backwards compatibility w/ `savedPrompts`, infinite gen, etc.
    /// We won't need this when we are able to migrate to using the underlying `Prompt` object as the source of truth.
    /// Unfortunately, this bridging means that Simple mode + Lyrics are going to populate a Custom mode gen. For infinite gen this might be okay.
    func toPrompt() -> Prompt {
        var prompt = Prompt()
        prompt.title = self.title
        prompt.lyrics = self.lyrics
        prompt.instrumental = self.lyricsMode.isInstrumental
        prompt.styles = self.styleText
        prompt.excludeStyles = self.excludeStyleText
        prompt.text = self.description
        prompt.coverClipId = self.coverClipId
        prompt.continueClipId = self.continueClipId
        prompt.continueAt = self.continueAt
        prompt.includeHistory = self.includeHistory
        prompt.audioRecording = self.audioRecording
        prompt.generationType = self.generationType
        prompt.token = self.token
        prompt.task = self.task
        prompt.audioRecording = self.audioRecording // See TODO above in `init(from prompt:)`
        prompt.createMode = self.createMode.toString()
        prompt.styleWeight = self.styleWeight
        prompt.weirdnessConstraint = self.weirdnessConstraint
        prompt.lyricsModel = self.lyricsModel

        return prompt
    }
}

// MARK: Identity Helper

extension BlendedCreatePrompt {
    static let empty = BlendedCreatePrompt()
    static let emptyInCustom: BlendedCreatePrompt = {
        var prompt = BlendedCreatePrompt.empty
        prompt.createMode = .custom // Set to custom to match the empty state in custom mode
        prompt.lyricsMode = .auto
        return prompt
    }()

    static let emptyInCustomWrite: BlendedCreatePrompt = {
        var prompt = BlendedCreatePrompt.empty
        prompt.createMode = .custom // Set to custom to match the empty state in custom mode
        prompt.lyricsMode = .write // Set to write because it's valid
        return prompt
    }()

    public var isEmpty: Bool {
        return self == BlendedCreatePrompt.empty || self == BlendedCreatePrompt.emptyInCustom || self == BlendedCreatePrompt.emptyInCustomWrite
    }
}

// MARK: Delete Audio Recording

public extension BlendedCreatePrompt {
    mutating func deleteAudioRecording() {
        guard let audioRecording else { return }
        try? FileManager.default.removeItem(at: audioRecording.url)
        self.audioRecording = nil
    }
}
