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

export interface BlogPostFrontMatter {
    title: string
    slug: string
    shortId?: 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<TFrontMatter extends BlogPostFrontMatter = BlogPostFrontMatter> = TFrontMatter & {
    file: string
    markdown: string
}

export interface BlogBackend<TFrontMatter extends BlogPostFrontMatter> {
    routePrefix: string,
    resourcePath: string[],
    author: {
        name: string,
        email: string,
        link: string,
    },
}

@Injectable()
export abstract class AbstractBlog<TFrontMatter extends BlogPostFrontMatter> {
    @Inject()
    protected readonly logging!: Logging

    @Inject()
    protected readonly routing!: Routing

    protected posts: Maybe<Collection<BlogPost<TFrontMatter>>>

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

    protected cachedFeed: Maybe<RSSFeed.Feed>

    protected abstract getBackend(): BlogBackend<TFrontMatter>

    protected abstract isValidFrontMatter(what: unknown): what is TFrontMatter

    async getAllPosts(): Promise<Collection<BlogPost<TFrontMatter>>> {
        if ( !this.posts ) {
            this.posts = collect()
            const path = appPath('resources', ...this.getBackend().resourcePath)
            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 ( !this.isValidFrontMatter(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')
                .tap(c => this.computeShortIds(c))
        }

        return this.posts
    }

    computeShortIds(posts: Collection<BlogPost<TFrontMatter>>): Collection<BlogPost<TFrontMatter>> {
        const ShortUniqueId = require('short-unique-id')
        const uid = new ShortUniqueId({ length: 6, counter: 0, shuffle: false })
        const usedIds: {[id: string]: boolean} = {}

        posts.sortBy('date')
            .each(post => {
                let shortId = uid.sequentialUUID()
                let tries = 10
                while ( usedIds[shortId] && tries > 0 ) {
                    shortId = uid.sequentialUUID()
                    tries -= 1
                }

                if ( usedIds[shortId] ) {
                    throw new Error('Could not recover from collision when generating blog post short ID')
                }

                usedIds[shortId] = true
                post.shortId = shortId
            })

        return posts
    }

    getPost(slug: string): Promise<Maybe<BlogPost<TFrontMatter>>> {
        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
        }

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

    protected abstract createFeed(lastUpdated: Maybe<Date>): Awaitable<RSSFeed.Feed>

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

        const posts = await this.getAllPosts()
        const feed = await this.createFeed(posts.whereMax('date').first()?.date)

        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: [this.getBackend().author],
            })
        }).awaitAll()

        this.cachedFeed = feed
        return feed
    }

    getUrl(post: BlogPost<TFrontMatter>): 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}`
        let prefix = this.getBackend().routePrefix
        if ( !prefix.startsWith('/') ) prefix = `/${prefix}`
        return `${prefix}/${year}/${month}/${day}/${post.slug}/`
    }
}
