import type { TlErrors } from '@mtcute/tl-utils'

import { writeFile } from 'node:fs/promises'
import { ffetchBase as ffetch } from '@fuman/fetch'
import { parse } from 'csv-parse/sync'

import { ERRORS_JSON_FILE } from './constants.js'

const ERRORS_PAGE_TG = 'https://corefork.telegram.org/api/errors'
const ERRORS_PAGE_TELETHON
    = 'https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_generator/data/errors.csv'
const baseErrors = {
    BAD_REQUEST: 400,
    UNAUTHORIZED: 401,
    FORBIDDEN: 403,
    NOT_FOUND: 404,
    FLOOD: 420,
    SEE_OTHER: 303,
    NOT_ACCEPTABLE: 406,
    INTERNAL: 500,
} as const

interface TelegramErrorsSpec {
    errors: Record<string, Record<string, string[]>>
    descriptions: Record<string, string>
    user_only: string[]
    bot_only: string[]
}

async function fetchFromTelegram(errors: TlErrors) {
    const page = await ffetch(ERRORS_PAGE_TG).text()
    const jsonUrl = page.match(/can be found <a href="([^"]+)">here »<\/a>/i)?.[1]
    if (!jsonUrl) throw new Error('Cannot find JSON URL')

    const json = await ffetch(new URL(jsonUrl, ERRORS_PAGE_TG).href).json<TelegramErrorsSpec>()

    // since nobody fucking guarantees that .descriptions
    // will have description for each described here (or vice versa),
    // we will process them independently

    for (const code of Object.keys(json.errors)) {
        for (const name of Object.keys(json.errors[code])) {
            const thrownBy = json.errors[code][name]

            const _code = Number.parseInt(code)

            if (Number.isNaN(_code)) {
                throw new TypeError(`Invalid code: ${code}`)
            }

            if (!(name in errors.errors)) {
                errors.errors[name] = {
                    code: _code,
                    name,
                }
            }

            for (const method of thrownBy) {
                if (!(method in errors.throws)) {
                    errors.throws[method] = []
                }

                if (!errors.throws[method].includes(name)) {
                    errors.throws[method].push(name)
                }
            }
        }
    }

    for (const name of Object.keys(json.descriptions)) {
        if (!(name in errors.errors)) {
            errors.errors[name] = {
                _auto: true,
                code: 400,
                name,
            }
        }

        errors.errors[name].description = json.descriptions[name]
    }

    json.user_only.forEach((it: string) => (errors.userOnly[it] = 1))
    json.bot_only.forEach((it: string) => (errors.botOnly[it] = 1))

    // process _* wildcard errors
    // 1. add description to errors that are missing it
    // 2. replace all wildcards in errors.throws with all matching errors
    // 3. remove all _* such errors from errors.errors

    for (const name of Object.keys(errors.errors)) {
        if (!name.endsWith('_*')) continue

        const base = name.slice(0, -2)

        const matchingErrors: string[] = []

        for (const inner of Object.keys(errors.errors)) {
            if (!inner.startsWith(base) || inner === name) continue

            matchingErrors.push(inner)

            if (!errors.errors[inner].description) {
                errors.errors[inner].description = errors.errors[name].description
            }
        }

        if (matchingErrors.length === 0) continue

        for (const method of Object.keys(errors.throws)) {
            const idx = errors.throws[method].indexOf(name)
            if (idx === -1) continue

            errors.throws[method].splice(idx, 1, ...matchingErrors)
        }

        delete errors.errors[name]
    }

    // clean up: remove duplicates in throws
    for (const method of Object.keys(errors.throws)) {
        errors.throws[method] = [...new Set(errors.throws[method])]
    }
}

async function fetchFromTelethon(errors: TlErrors) {
    const csv = await ffetch(ERRORS_PAGE_TELETHON).text()

    const records = parse(csv, {
        columns: true,
        skip_empty_lines: true,
    }) as {
        name: string
        codes: string
        description: string
    }[]

    for (const { name: name_, codes, description } of records) {
        if (!codes) continue
        if (name_ === 'TIMEOUT') continue

        let name = name_
        const code = Number.parseInt(codes)

        if (Number.isNaN(code)) {
            throw new TypeError(`Invalid code: ${codes} (name: ${name})`)
        }

        // telethon uses X for parameters instead of printf-like
        // we'll convert it back to printf-like
        name = name.replace(/_X(_)?/g, '_%d$1')

        if (!(name in errors.errors)) {
            errors.errors[name] = {
                code,
                name,
            }
        }

        const obj = errors.errors[name]

        if (obj._auto) {
            obj.code = code
            delete obj._auto
        }

        // same with descriptions, telethon uses python-like formatting
        // strings. we'll convert it to printf-like, while also saving parameter
        // names for better code insights
        // we also prefer description from telegram, if it's available and doesn't use placeholders
        if (description) {
            const desc = description.replace(/\{(\w+)\}/g, (_, name: string) => {
                if (!obj._paramNames) {
                    obj._paramNames = []
                }
                obj._paramNames.push(name)

                return '%d'
            })

            if (!obj.description || obj._paramNames?.length) {
                obj.description = desc
            }
        }
    }
}

async function main() {
    const errors: TlErrors = {
        base: baseErrors,
        errors: {},
        throws: {},
        userOnly: {},
        botOnly: {},
    }

    console.log('Fetching errors from Telegram...')
    await fetchFromTelegram(errors)

    // using some incredible fucking crutches we are also able to parse telethon errors file
    // and add missing error descriptions
    console.log('Fetching errors from Telethon...')
    await fetchFromTelethon(errors)

    console.log('Saving...')

    await writeFile(ERRORS_JSON_FILE, JSON.stringify(errors))
}

main().catch(console.error)
