import axios, { AxiosInstance } from 'axios'
import { load } from 'cheerio'
import { CookieJar } from 'tough-cookie'
import { HttpsCookieAgent } from 'http-cookie-agent/http'
import { parseDoujinList, parseDoujinInfo } from '../../Parser'
import { sites } from '../constants'
import { IDoujinInfo, TSite, IList } from '../../Types'
import { getAPIGalleryPages } from '../util'

export class NHentai {
    #axios: AxiosInstance

    /**
     * Helper method to extract site name from URL
     */
    private getSiteName(): 'to' | 'net' | 'website' {
        const siteName = this._options.site
            .replace('https://', '')
            .replace('http://', '')
            .replace(/\/$/, '')

        if (siteName.includes('nhentai.to')) return 'to'
        if (siteName.includes('nhentai.net')) return 'net'
        if (siteName.includes('nhentai.website')) return 'website'

        // Default fallback
        return 'to'
    }

    /**
     * Constructs an instance of the NHentai class
     * @param _options Options of the NHentai class
     */
    constructor(
        private readonly _options: {
            site: TSite | `https://${TSite}`
            user_agent?: string
            cookie_value?: string
        } = {
            site: 'https://nhentai.to'
        }
    ) {
        this.#axios = axios
        // Extract the site name from the URL
        const siteName = this._options.site
            .replace('https://', '')
            .replace('http://', '')
            .replace(/\/$/, '') as TSite

        if (!sites.includes(siteName)) {
            this._options.site = 'https://nhentai.to'
        }
        if (!this._options.site.startsWith('https://'))
            this._options.site =
                `https://${this._options.site}` as `https://${TSite}`
        if (
            this._options.site.includes('nhentai.net') &&
            (!this._options.cookie_value || !this._options.user_agent)
        )
            throw new Error(
                `Assign the ${
                    !this._options.cookie_value
                        ? "'cookie_value'"
                        : "'user_agent'"
                } in the instance of the class to use this site.`
            )
        if (this._options.cookie_value) {
            const jar = new CookieJar()
            jar.setCookie(this._options.cookie_value, this._options.site)
            const httpsAgent = new HttpsCookieAgent({ cookies: { jar } })
            this.#axios = axios.create({ httpsAgent })
        }
        if (this._options.user_agent)
            this.#axios.defaults.headers.common['User-Agent'] =
                this._options.user_agent
    }

    /**
     * Gets a random doujin
     * @returns Info of the random doujin
     */
    public getRandom = async (): Promise<IDoujinInfo> =>
        await this.#axios
            .get<string>(`${this._options.site}/random`)
            .then(async ({ data }) =>
                parseDoujinInfo(
                    load(data),
                    this.getSiteName(),
                    this._options.site.includes('net')
                        ? await getAPIGalleryPages(this.#axios, data)
                        : undefined
                )
            )
            .catch((err) => {
                throw new Error(err.message)
            })

    /**
     * Explores the list of doujin
     * @param page Page number of the list
     * @returns The doujin list
     */
    public explore = async (page = 1): Promise<IList> => {
        if (isNaN(page) || page < 1) page = 1
        return await this.#axios
            .get<string>(`${this._options.site}?page=${page}`)
            .then(({ data }) => parseDoujinList(load(data), this.getSiteName()))
            .catch((err) => {
                throw new Error(err.message)
            })
    }

    /**
     * Searches for a doujin by a query
     * @param query Query of the doujin to search
     * @param options Options for searching
     * @returns The result of the search
     */
    public search = async (
        query: string,
        options?: { page?: number }
    ): Promise<IList> => {
        if (!query)
            throw new Error("The 'query' parameter shouldn't be undefined")
        let page = 1
        if (options?.page && options.page > 0) page = options.page
        return await this.#axios
            .get<string>(`${this._options.site}/search?q=${query}&page=${page}`)
            .then((res) => {
                const results = parseDoujinList(
                    load(res.data),
                    this.getSiteName()
                )
                if (!results.data.length)
                    throw new Error('No search results found')
                return results
            })
    }

    /**
     * Searches Tag for a doujin by a query
     * @param query Tag of the doujin to search
     * @param options Options for searching
     * @returns The result of the search
     */
    public searchWithTag = async (
        query: string,
        options?: { page?: number }
    ): Promise<IList> => {
        if (!query)
            throw new Error("The 'query' parameter shouldn't be undefined")
        let page = 1
        if (options?.page && options.page > 0) page = options.page
        return await this.#axios
            .get<string>(
                `${this._options.site}/tag/${query}${
                    page > 1 ? `?page=${page}` : ''
                }`
            )
            .then((res) => {
                const results = parseDoujinList(
                    load(res.data),
                    this.getSiteName()
                )
                if (!results.data.length)
                    throw new Error('No tags results found')
                return results
            })
    }

    /**
     * Searches artist for a doujin by a query
     * @param query Artist of the doujin to search
     * @param options Options for searching
     * @returns The result of the search
     */
    public searchWithArtist = async (
        query: string,
        options?: { page?: number }
    ): Promise<IList> => {
        if (!query)
            throw new Error("The 'query' parameter shouldn't be undefined")
        let page = 1
        if (options?.page && options.page > 0) page = options.page
        return await this.#axios
            .get<string>(
                `${this._options.site}/artist/${query}${
                    page > 1 ? `?page=${page}` : ''
                }`
            )
            .then((res) => {
                const results = parseDoujinList(
                    load(res.data),
                    this.getSiteName()
                )
                if (!results.data.length)
                    throw new Error('No artists results found')
                return results
            })
    }

    /**
     * Searches parody for a doujin by a query
     * @param query Parody of the doujin to search
     * @param options Options for searching
     * @returns The result of the search
     */
    public searchWithParody = async (
        query: string,
        options?: { page?: number }
    ): Promise<IList> => {
        if (!query)
            throw new Error("The 'query' parameter shouldn't be undefined")
        let page = 1
        if (options?.page && options.page > 0) page = options.page
        return await this.#axios
            .get<string>(
                `${this._options.site}/parody/${query}${
                    page > 1 ? `?page=${page}` : ''
                }`
            )
            .then((res) => {
                const results = parseDoujinList(
                    load(res.data),
                    this.getSiteName()
                )
                if (!results.data.length)
                    throw new Error('No parodies results found')
                return results
            })
    }

    /**
     * Searches character for a doujin by a query
     * @param query Character of the doujin to search
     * @param options Options for searching
     * @returns The result of the search
     */
    public searchWithCharacter = async (
        query: string,
        options?: { page?: number }
    ): Promise<IList> => {
        if (!query)
            throw new Error("The 'query' parameter shouldn't be undefined")
        let page = 1
        if (options?.page && options.page > 0) page = options.page
        return await this.#axios
            .get<string>(
                `${this._options.site}/character/${query}${
                    page > 1 ? `?page=${page}` : ''
                }`
            )
            .then((res) => {
                const results = parseDoujinList(
                    load(res.data),
                    this.getSiteName()
                )
                if (!results.data.length)
                    throw new Error('No characters results found')
                return results
            })
    }

    /**
     * Gets the info of a doujin by its ID
     * @param id ID of the doujin
     * @returns Info of the doujin
     */
    public getDoujin = async (id: string | number): Promise<IDoujinInfo> => {
        if (!id) throw new Error("The 'id' parameter shouldn't be undefined")
        const valid = await this.validate(id)
        if (!valid) throw new Error('Invalid doujin ID')
        return await this.#axios
            .get(`${this._options.site}/g/${id}`)
            .then(async (res) =>
                parseDoujinInfo(
                    load(res.data),
                    this.getSiteName(),
                    this._options.site.includes('net')
                        ? await getAPIGalleryPages(this.#axios, res.data)
                        : undefined
                )
            )
            .catch((err) => {
                throw new Error(err.message)
            })
    }

    /**
     * Validates the ID of a doujin
     * @param id ID of the doujin to check
     */
    public validate = (id: string | number): Promise<boolean> =>
        this.#axios
            .get(`${this._options.site}/g/${id}`)
            .then(() => true)
            .catch(() => false)
}
