import Foundation
import os.log

private let logger = Logger(subsystem: "com.regionmedia.theriotact", category: "model+codable")

extension AuthResponse: Codable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case avatar
        case link
        case email
        case token
        case refreshToken
        case deleteStatus = "delete-status"
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        avatar = try? container.decode(URL.self, forKey: .avatar)
        link = try container.decode(String.self, forKey: .link)
        email = try container.decode(String.self, forKey: .email)
        token = try container.decode(String.self, forKey: .token)
        refreshToken = try container.decode(String.self, forKey: .refreshToken)
        
        deleteStatus = try container.decode(DeleteStatus.self, forKey: .deleteStatus)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(avatar, forKey: .avatar)
        try container.encode(link, forKey: .link)
        try container.encode(email, forKey: .email)
        try container.encode(token, forKey: .token)
        try container.encode(refreshToken, forKey: .refreshToken)
        try container.encode(deleteStatus, forKey: .deleteStatus)
    }
}

extension Topic: Codable {
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case slug
        case link
        case colour
        case textColour = "text_colour"
        case children
        case featurePosts = "posts"
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try container.decode(Int.self, forKey: .id)
        slug = try container.decode(String.self, forKey: .slug)
        link = try? container.decode(URL.self, forKey: .link)
        let name = try container.decode(String.self, forKey: .name)
        self.name = name.removeHTML
        
        colour = try container.decode(String.self, forKey: .colour)
        textColour = try container.decode(String.self, forKey: .textColour)
        children = (try? container.decode([Topic].self, forKey: .children)) ?? []
        featurePosts = (try? container.decode([Post].self, forKey: .featurePosts)) ?? []
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(slug, forKey: .slug)
        try container.encode(link, forKey: .link)
        try container.encode(colour, forKey: .colour)
        try container.encode(textColour, forKey: .textColour)
        try container.encode(children, forKey: .children)
        try container.encode(featurePosts, forKey: .featurePosts)
    }
}

extension Comment: Codable {
    enum CodingKeys: String, CodingKey {
        case source
        case id
        case name
        case avatar
        case date
        case dateGMT = "date_gmt"
        case link
        case content
        case excerpt
        case post
        case children
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        if let idString = try? container.decode(String.self, forKey: .id) {
            guard let idInt = Int(idString) else {
                throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.id], debugDescription: "Expecting int value for ID; Got \(idString)", underlyingError: nil))
            }
            id = idInt
        } else {
            id = try container.decode(Int.self, forKey: .id)
        }
        
        source = try container.decode(Comment.Source.self, forKey: .source)
        avatar = try container.decode(URL.self, forKey: .avatar)
        let name = try container.decode(String.self, forKey: .name)
        self.name = name.removeHTML
        
        date = try container.decode(Date.self, forKey: .date)
        dateGMT = try? container.decode(Date.self, forKey: .dateGMT)
        link = try container.decode(URL.self, forKey: .link)
        let content = try container.decode(String.self, forKey: .content)
        self.content = content.removeHTML
        
        if let excerpt = try? container.decode(String.self, forKey: .excerpt) {
            self.excerpt = excerpt.removeHTML
        } else if let truncated = self.content.components(separatedBy: ".").first, truncated != self.content {
            self.excerpt = truncated + "..."
        } else {
            self.excerpt = self.content
        }
        
        post = try? container.decodeIfPresent(String.self, forKey: .post)
        
        children = (try? container.decodeIfPresent([Comment].self, forKey: .children)) ?? []
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
        try container.encode(source, forKey: .source)
        try container.encode(link, forKey: .link)
        try container.encode(avatar, forKey: .avatar)
        try container.encode(date, forKey: .date)
        try container.encode(dateGMT, forKey: .dateGMT)
        try container.encode(content, forKey: .content)
        try container.encodeIfPresent(post, forKey: .post)
        try container.encode(children, forKey: .children)
    }
}

extension Banner: Codable {
    enum CodingKeys: String, CodingKey {
        case name, heading, subheading, link, banner, logo
        case hideShade = "hide_shade"
        case headingSize = "heading_size"
        case subheadingSize = "subheading_size"
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        name = try container.decode(String.self, forKey: .name)
        hideShade = try container.decode(Bool.self, forKey: .hideShade)
        heading = (try? container.decode(String.self, forKey: .heading))?.removeHTML
        subheading = (try? container.decode(String.self, forKey: .subheading))?.removeHTML
        link = try container.decode(URL.self, forKey: .link)
        banner = try container.decode(URL.self, forKey: .banner)
        logo = try container.decode(URL.self, forKey: .logo)
        
        if let headingSizeString = try? container.decode(String.self, forKey: .headingSize) {
            headingSize = Int(headingSizeString.numeric)
        }
        
        if let subheadingSizeString = try? container.decode(String.self, forKey: .subheadingSize) {
            subheadingSize = Int(subheadingSizeString.numeric)
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(name, forKey: .name)
        try container.encode(hideShade, forKey: .hideShade)
        try container.encode(heading, forKey: .heading)
        try container.encode(subheading, forKey: .subheading)
        try container.encode(link, forKey: .link)
        try container.encode(banner, forKey: .banner)
        try container.encode(logo, forKey: .logo)
        
        if let headingSize = self.headingSize {
            try container.encode("\(headingSize)px", forKey: .headingSize)
        }
        if let subheadingSize = self.subheadingSize {
            try container.encode("\(subheadingSize)px", forKey: .subheadingSize)
        }
        
    }
}

extension Post: Codable {
    enum CodingKeys: String, CodingKey {
        case id
        case date
        case dateGMT = "date_gmt"
        case modified
        case modifiedGMT = "modified_gmt"
        case slug
        case type
        case link
        case title
        case content
        case excerpt
        case author
        case commentStatus = "comment_status"
        case topics = "categories"
        case tags
        case primaryTopic = "primary_category"
        case commentCount = "comment_count"
        case thumbnail
        case events
        case allComments = "comments"
        case latestComments = "latest_comments"
        case banners
        case webAssets = "assets"
        case relatedPosts = "related_posts"
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try container.decode(Int.self, forKey: .id)
        date = try? container.decode(Date.self, forKey: .date)
        dateGMT = try? container.decode(Date.self, forKey: .dateGMT)
        slug = try container.decode(String.self, forKey: .slug)
        type = try container.decode(String.self, forKey: .type)
        link = try? container.decode(URL.self, forKey: .link)
        let title = try container.decode(Rendered.self, forKey: .title)
        self.title = Rendered(rendered: title.rendered.removeHTML, protected: title.protected)
        
        author = try container.decode([Author].self, forKey: .author)
        if let commentCountString = try? container.decode(String.self, forKey: .commentCount) {
            commentCount = Int(commentCountString) ?? 0
        } else {
            commentCount = try container.decode(Int.self, forKey: .commentCount) 
        }
        primaryTopic =  try? container.decode(Topic.self, forKey: .primaryTopic)
        
        modified = try? container.decode(Date.self, forKey: .modified)
        modifiedGMT = try? container.decode(Date.self, forKey: .modifiedGMT)
        content = try container.decode(Rendered.self, forKey: .content)
        excerpt = try container.decode(Rendered.self, forKey: .excerpt)
        commentStatus = try container.decode(CommentStatus.self, forKey: .commentStatus)
        topics = try container.decode([TopicRef].self, forKey: .topics)
        tags = try container.decode([Tag].self, forKey: .tags)
        thumbnail = (try? container.decode(Thumbnail.self, forKey: .thumbnail)) ?? Thumbnail(square: nil, banner: nil)
        
        latestComments = (try? container.decode([Comment].self, forKey: .latestComments)) ?? []
        
        do {
            self.allComments = try container.decodeIfPresent([Comment].self, forKey: .allComments)
        } catch {
            logger.error("\(error.localizedDescription)")
            self.allComments = nil
        }
        
        do {
            let eventDictionary = try container.decode([String: Post.Event].self, forKey: .events)
            events = Array(eventDictionary.values)
        } catch {
            events = []
        }
        
        do {
            self.banners = try container.decodeIfPresent([Banner].self, forKey: .banners)
        } catch {
            logger.error("\(error.localizedDescription)")
            self.banners = nil
        }
        
        if let assetsDictionary = try? container.decodeIfPresent([String:URL].self, forKey: .webAssets) {
            self.webAssets = assetsDictionary.compactMap({ key, value in
                WebAsset(key: key, url: value)
            })
        } else {
            self.webAssets = []
        }
        
        do {
            relatedPosts = try container.decode([Post].self, forKey: .relatedPosts)
        } catch {
            logger.error("\(error.localizedDescription)")
            self.relatedPosts = []
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(id, forKey: .id)
        if let date = date {
            try container.encode(date, forKey: .date)
        }
        if let dateGMT = dateGMT {
            try container.encode(dateGMT, forKey: .dateGMT)
        }
        if let modified = modified {
            try container.encode(modified, forKey: .modified)
        }
        if let modifiedGMT = modifiedGMT {
            try container.encode(modifiedGMT, forKey: .modifiedGMT)
        }
        try container.encode(slug, forKey: .slug)
        try container.encode(type, forKey: .type)
        try container.encode(link, forKey: .link)
        try container.encode(title, forKey: .title)
        try container.encode(excerpt, forKey: .excerpt)
        try container.encode(title, forKey: .title)
        try container.encode(author, forKey: .author)
        try container.encode(commentStatus, forKey: .commentStatus)
        
        try container.encode(primaryTopic, forKey: .primaryTopic)
        try container.encode(topics, forKey: .topics)
        try container.encode(tags, forKey: .tags)
        try container.encode(commentCount, forKey: .commentCount)
        try container.encode(thumbnail, forKey: .thumbnail)
        try container.encode(events, forKey: .events)
        
        try container.encode(latestComments, forKey: .latestComments)
        try container.encode(allComments, forKey: .allComments)
        try container.encode(banners, forKey: .banners)
    }
}

extension Post.Event: Codable {
    enum CodingKeys: String, CodingKey {
        case date = "event_date"
        case timeStart = "event_start"
        case timeEnd = "event_end"
        case location
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        let dateString = try container.decode(String.self, forKey: .date)
        if let date = JSONDecoder.DateDecodingStrategy.formatter.date(from: dateString+"T00:00:00") {
            self.date = date
        } else {
            throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.date], debugDescription: "Expecting a string value of YYYY-MM-DD; Got \(dateString)", underlyingError: nil))
        }
        self.timeStart = try container.decodeIfPresent(String.self, forKey: .timeStart)
        self.timeEnd = try container.decodeIfPresent(String.self, forKey: .timeEnd)
        self.location = try container.decodeIfPresent(String.self, forKey: .location)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        // TODO - make this better represent incoming
        try container.encode(date, forKey: .date)
        try container.encodeIfPresent(timeStart, forKey: .timeStart)
        try container.encodeIfPresent(timeEnd, forKey: .timeEnd)
        try container.encodeIfPresent(location, forKey: .location)
    }
}

extension PostSummary: Codable {
    enum CodingKeys: String, CodingKey {
        case id
        case date
        case dateGMT = "date_gmt"
        case slug
        case type
        case link
        case title
        case author
        case commentCount = "comment_count"
        case commentStatus = "comment_status"
        case latestCommentDate = "latest_comment"
        case primaryTopic = "primary_category"
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try container.decode(Int.self, forKey: .id)
        date = try? container.decode(Date.self, forKey: .date)
        dateGMT = try? container.decode(Date.self, forKey: .dateGMT)
        slug = try container.decode(String.self, forKey: .slug)
        type = try container.decode(String.self, forKey: .type)
        link = try? container.decode(URL.self, forKey: .link)
        let title = try container.decode(String.self, forKey: .title)
        self.title = title.removeHTML
        
        author = try container.decode([Author].self, forKey: .author)
        let commentCountString = try container.decode(String.self, forKey: .commentCount)
        commentCount = Int(commentCountString) ?? 0
        commentStatus = (try? container.decode(CommentStatus.self, forKey: .commentStatus)) ?? .open
        
        latestCommentDate = try? container.decode(Date.self, forKey: .latestCommentDate)
        primaryTopic =  try? container.decode(Topic.self, forKey: .primaryTopic)
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encode(id, forKey: .id)
        if let date = date {
            try container.encode(date, forKey: .date)
        }
        if let dateGMT = dateGMT {
            try container.encode(dateGMT, forKey: .dateGMT)
        }
        try container.encode(slug, forKey: .slug)
        try container.encode(type, forKey: .type)
        try container.encode(link, forKey: .link)
        try container.encode(title, forKey: .title)
        try container.encode(author, forKey: .author)
        try container.encode("\(commentCount)", forKey: .commentCount)
        try container.encode(latestCommentDate, forKey: .latestCommentDate)
        try container.encode(primaryTopic, forKey: .primaryTopic)
        
    }
}

extension EventCalendar: Codable {
    private typealias TimeEntry = [String: [String]]
    private typealias Days = [String: TimeEntry]
    private typealias PostDictionary = [String: Post]

    enum CodingKeys: String, CodingKey {
        case events
        case posts
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let dateFormatter = JSONDecoder.DateDecodingStrategy.formatter
        let postDict = try container.decode(PostDictionary.self, forKey: .posts)
        posts = Array(postDict.values)

        let days: Days = try container.decode(Days.self, forKey: .events)
        
        var timeslots: [TimeSlot] = []
        
        days.forEach { (dateString, timeEntries) in
            timeEntries.forEach { (timeString, postIds) in
                if let date = dateFormatter.date(from: "\(dateString)T\(timeString)") {
                    let timeSlot = TimeSlot(time: date, postIds: postIds)
                    timeslots.append(timeSlot)
                }
            }
        }
        
        self.schedule = timeslots
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(posts, forKey: .posts)
        // TODO: Encode events

    }
}

extension EventCalendar.TimeSlot: Comparable {
    public static func < (lhs: EventCalendar.TimeSlot, rhs: EventCalendar.TimeSlot) -> Bool {
        lhs.time < rhs.time
    }
    
    
}

extension PolicyPageResponse: Decodable {
    struct TitleKey: CodingKey {
        var stringValue: String
        var intValue: Int?

        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        init?(intValue: Int) {
            self.stringValue = "\(intValue)"
            self.intValue = intValue
        }
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: TitleKey.self)
        
        var pages: [PolicyPage] = []
        
        for key in container.allKeys {
            if let page = try? container.decode(Post.self, forKey: key) {
                pages.append(PolicyPage(navigationTitle: key.stringValue, post: page))
            }
        }
        
        self.pages = pages
    }
}
