//
//  Status.swift
//  Pachyderm
//
//  Created by Shadowfacts on 9/9/18.
//  Copyright © 2018 Shadowfacts. All rights reserved.
//

import Foundation
import WebURL

public final class Status: StatusProtocol, Decodable, Sendable {
    /// The pseudo-visibility used by instance types (Akkoma) that overload the visibility for local-only posts.
    public static let localPostVisibility: String = "local"
    
    public let id: String
    public let uri: String
    public let url: WebURL?
    public let account: Account
    public let inReplyToID: String?
    public let inReplyToAccountID: String?
    public let reblog: Status?
    public let content: String
    public let createdAt: Date
    public let emojis: [Emoji]
    // TODO: missing from pleroma
//    public let repliesCount: Int
    public let reblogsCount: Int
    public let favouritesCount: Int
    public let reblogged: Bool?
    public let favourited: Bool?
    public let muted: Bool?
    public let sensitive: Bool
    public let spoilerText: String
    public let visibility: Visibility
    public let attachments: [Attachment]
    public let mentions: [Mention]
    public let hashtags: [Hashtag]
    public let application: Application?
    public let language: String?
    public let pinned: Bool?
    public let bookmarked: Bool?
    public let card: Card?
    public let poll: Poll?
    // Hometown, Glitch only
    public let localOnly: Bool?
    public let editedAt: Date?
    
    public var applicationName: String? { application?.name }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.uri = try container.decode(String.self, forKey: .uri)
        do {
            self.url = try container.decodeIfPresent(WebURL.self, forKey: .url)
        } catch {
            let s = try? container.decode(String.self, forKey: .url)
            if s == "" {
                self.url = nil
            } else {
                throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "<failed to decode string>")'", underlyingError: error))
            }
        }
        self.account = try container.decode(Account.self, forKey: .account)
        self.inReplyToID = try container.decodeIfPresent(String.self, forKey: .inReplyToID)
        self.inReplyToAccountID = try container.decodeIfPresent(String.self, forKey: .inReplyToAccountID)
        self.reblog = try container.decodeIfPresent(Status.self, forKey: .reblog)
        self.content = try container.decode(String.self, forKey: .content)
        self.createdAt = try container.decode(Date.self, forKey: .createdAt)
        self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? []
        self.reblogsCount = try container.decode(Int.self, forKey: .reblogsCount)
        self.favouritesCount = try container.decode(Int.self, forKey: .favouritesCount)
        self.reblogged = try container.decodeIfPresent(Bool.self, forKey: .reblogged)
        self.favourited = try container.decodeIfPresent(Bool.self, forKey: .favourited)
        self.muted = try container.decodeIfPresent(Bool.self, forKey: .muted)
        self.sensitive = try container.decode(Bool.self, forKey: .sensitive)
        self.spoilerText = try container.decode(String.self, forKey: .spoilerText)
        if let visibility = try? container.decode(Visibility.self, forKey: .visibility) {
            self.visibility = visibility
            self.localOnly = try container.decodeIfPresent(Bool.self, forKey: .localOnly)
        } else if let s = try? container.decode(String.self, forKey: .visibility),
                  s == Status.localPostVisibility {
            // hacky workaround for #332, akkoma describes local posts with a separate visibility
            self.visibility = .public
            self.localOnly = true
        } else {
            throw DecodingError.dataCorruptedError(forKey: .visibility, in: container, debugDescription: "Could not decode visibility")
        }
        self.attachments = try container.decode([Attachment].self, forKey: .attachments)
        self.mentions = try container.decode([Mention].self, forKey: .mentions)
        self.hashtags = try container.decode([Hashtag].self, forKey: .hashtags)
        self.application = try container.decodeIfPresent(Application.self, forKey: .application)
        self.language = try container.decodeIfPresent(String.self, forKey: .language)
        self.pinned = try container.decodeIfPresent(Bool.self, forKey: .pinned)
        self.bookmarked = try container.decodeIfPresent(Bool.self, forKey: .bookmarked)
        self.card = try container.decodeIfPresent(Card.self, forKey: .card)
        self.poll = try container.decodeIfPresent(Poll.self, forKey: .poll)
        self.editedAt = try container.decodeIfPresent(Date.self, forKey: .editedAt)
    }
    
    public static func getContext(_ statusID: String) -> Request<ConversationContext> {
        return Request<ConversationContext>(method: .get, path: "/api/v1/statuses/\(statusID)/context")
    }
    
    public static func getCard(_ status: Status) -> Request<Card> {
        return Request<Card>(method: .get, path: "/api/v1/statuses/\(status.id)/card")
    }
    
    public static func getFavourites(_ statusID: String, range: RequestRange = .default) -> Request<[Account]> {
        var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/favourited_by")
        request.range = range
        return request
    }
    
    public static func getReblogs(_ statusID: String, range: RequestRange = .default) -> Request<[Account]> {
        var request = Request<[Account]>(method: .get, path: "/api/v1/statuses/\(statusID)/reblogged_by")
        request.range = range
        return request
    }
    
    public static func delete(_ statusID: String) -> Request<Empty> {
        return Request<Empty>(method: .delete, path: "/api/v1/statuses/\(statusID)")
    }
    
    public static func reblog(_ statusID: String, visibility: Visibility? = nil) -> Request<Status> {
        var params: [Parameter] = []
        if let visibility {
            assert([.public, .unlisted, .private].contains(visibility))
            params.append("visibility" => visibility.rawValue)
        }
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/reblog", queryParameters: params)
    }
    
    public static func unreblog(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unreblog")
    }
    
    public static func favourite(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/favourite")
    }
    
    public static func unfavourite(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unfavourite")
    }
    
    public static func pin(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/pin")
    }
    
    public static func unpin(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unpin")
    }
    
    public static func bookmark(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/bookmark")
    }
    
    public static func unbookmark(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unbookmark")
    }
    
    public static func muteConversation(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/mute")
    }
    
    public static func unmuteConversation(_ statusID: String) -> Request<Status> {
        return Request<Status>(method: .post, path: "/api/v1/statuses/\(statusID)/unmute")
    }
    
    public static func source(_ statusID: String) -> Request<StatusSource> {
        return Request(method: .get, path: "/api/v1/statuses/\(statusID)/source")
    }
    
    public static func history(_ statusID: String) -> Request<[StatusEdit]> {
        return Request(method: .get, path: "/api/v1/statuses/\(statusID)/history")
    }
    
    private enum CodingKeys: String, CodingKey {
        case id
        case uri
        case url
        case account
        case inReplyToID = "in_reply_to_id"
        case inReplyToAccountID = "in_reply_to_account_id"
        case reblog
        case content
        case createdAt = "created_at"
        case emojis
//        case repliesCount = "replies_count"
        case reblogsCount = "reblogs_count"
        case favouritesCount = "favourites_count"
        case reblogged
        case favourited
        case muted
        case sensitive
        case spoilerText = "spoiler_text"
        case visibility
        case attachments = "media_attachments"
        case mentions
        case hashtags = "tags"
        case application
        case language
        case pinned
        case bookmarked
        case card
        case poll
        case localOnly = "local_only"
        case editedAt = "edited_at"
    }
}

extension Status: Identifiable {}
