import * as fs from 'fs/promises'
import * as matter from 'gray-matter'
import * as marked from 'marked'
import * as RSSFeed from 'feed'
import * as xml2js from 'xml2js'
import {Singleton, appPath, Collection, Inject, Maybe, collect, Logging, hasOwnProperty, Routing} from '@extollo/lib'

export interface BlogPostFrontMatter {
    title: string
    slug: string
    date: Date
    tags: string[]
}

export const isBlogPostFrontMatter = (what: unknown): what is BlogPostFrontMatter => {
    return typeof what === 'object' && what !== null
        && hasOwnProperty(what, 'title') && typeof what.title === 'string'
        && hasOwnProperty(what, 'slug') && typeof what.slug === 'string'
        && hasOwnProperty(what, 'date') && what.date instanceof Date
        && hasOwnProperty(what, 'tags') && Array.isArray(what.tags)
        && what.tags.every(tag => typeof tag === 'string')
}

export type BlogPost = BlogPostFrontMatter & {
    file: string
    markdown: string
}

export interface FeedSub {
    category: string
    name: string
    url: string
}

@Singleton()
export class Blog {
    @Inject()
    protected readonly logging!: Logging

    @Inject()
    protected readonly routing!: Routing

    protected posts: Maybe<Collection<BlogPost>>

    protected postRenderCache: Record<string, string> = {}

    protected cachedFeed: Maybe<RSSFeed.Feed>

    async getAllPosts(): Promise<Collection<BlogPost>> {
        if ( !this.posts ) {
            this.posts = collect()
            const path = appPath('resources', 'blog-posts')
            const contents = await fs.readdir(path.toLocal)
            for ( const file of contents ) {
                if ( !file.endsWith('.md') ) continue
                const filePath = path.concat(file)
                const fileContents = await filePath.read()
                const parsed = matter(fileContents)

                const front = parsed.data
                if ( !isBlogPostFrontMatter(front) ) {
                    this.logging.warn(`The following blog post had invalid front-matter: ${filePath}`)
                    continue
                }

                this.posts.push({
                    ...front,
                    file,
                    markdown: parsed.content,
                })
            }

            this.posts = this.posts.sortByDesc('date')
        }

        return this.posts
    }

    getPost(slug: string): Promise<Maybe<BlogPost>> {
        return this.getAllPosts()
            .then(p => p.firstWhere('slug', '=', slug))
    }

    async renderPost(slug: string): Promise<Maybe<string>> {
        const cached = this.postRenderCache[slug]
        if ( cached ) {
            return cached
        }

        const post = await this.getPost(slug)
        if ( !post ) {
            return undefined
        }

        const render = marked.marked(post.markdown)
        this.postRenderCache[slug] = render
        return render
    }

    async getFeed(): Promise<RSSFeed.Feed> {
        if ( this.cachedFeed ) {
            return this.cachedFeed
        }

        const posts = await this.getAllPosts()
        const feed = new RSSFeed.Feed({
            title: 'Garrett\'s Blog',
            description: 'Write-ups and musings by Garrett Mills, often technical, sometimes not',
            id: `${this.routing.getAppUrl()}#about`,
            link: this.routing.getNamedPath('blog').toRemote,
            language: 'en',
            image: this.routing.getAssetPath('favicon', 'apple-touch-icon.png').toRemote,
            favicon: this.routing.getAssetPath('favicon', 'favicon.ico').toRemote,
            copyright: `Copyright (c) ${(new Date).getFullYear()} Garrett Mills. See website for licensing details.`,
            updated: posts.whereMax('date').first()?.date,
            generator: '',
            feedLinks: {
                json: this.routing.getNamedPath('blog:json').toRemote,
                atom: this.routing.getNamedPath('blog:atom').toRemote,
                rss: this.routing.getNamedPath('blog:rss').toRemote,
            },
            author: {
                name: 'Garrett Mills',
                email: 'shout@garrettmills.dev',
                link: 'https://garrettmills.dev/#about',
            },
        })

        feed.addCategory('Technology')
        feed.addCategory('Software Development')

        await posts.map(async post => {
            feed.addItem({
                title: post.title,
                date: post.date,
                id: this.getUrl(post),
                link: this.getUrl(post),
                content: await this.renderPost(post.slug),
                author: [{
                    name: 'Garrett Mills',
                    email: 'shout@garrettmills.dev',
                    link: 'https://garrettmills.dev/#about',
                }],
            })
        }).awaitAll()

        this.cachedFeed = feed
        return feed
    }

    async getSubs(): Promise<Collection<FeedSub>> {
        const subs = collect<FeedSub>()
        const opml = await this.getOPML()
        for ( const row of opml.opml.body[0].outline ) {
            if ( !row.outline ) {
                subs.push({
                    category: '(uncategorized)',
                    name: row['$'].text,
                    url: row['$'].xmlUrl,
                })
                continue
            }

            let category = row['$'].text
            if ( category === 'Garrett Mills' ) category += ' (shameless plug)'
            for ( const sub of row.outline ) {
                subs.push({
                    category,
                    name: sub['$'].text,
                    url: sub['$'].xmlUrl,
                })
            }
        }
        return subs
    }

    async getOPML(): Promise<any> {
        const path = appPath('resources', 'assets', 'rss_opml.xml')
        const xmlContent = await path.read()
        return new Promise<any>((res, rej) => {
            xml2js.parseString(xmlContent, (err, result) => {
                if ( err ) {
                    rej(err)
                }

                res(result)
            })
        })
    }

    getUrl(post: BlogPost): string {
        const year = post.date.getFullYear()
        let month = String(post.date.getMonth() + 1)
        if ( month.length < 2 ) month = `0${month}`
        let day = String(post.date.getDate())
        if ( day.length < 2 ) day = `0${day}`
        return `/blog/${year}/${month}/${day}/${post.slug}/`
    }
}
