import { Route, Data } from '@/types';
import { art } from '@/utils/render';
import path from 'node:path';
import { Context } from 'hono';
import { Genre, GenreNotation, NarouNovelFetch, NovelTypeParam, Order, R18Site, SearchBuilder, SearchBuilderR18, SearchParams } from 'narou';
import queryString from 'query-string';
import { Join } from 'narou/util/type';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import { SyosetuSub, NarouSearchParams, syosetuSubToJapanese } from './types/search';

export const route: Route = {
    path: '/search/:sub/:query',
    categories: ['reading'],
    example: '/syosetu/search/noc/word=ハーレム&notword=&type=r&mintime=&maxtime=&minlen=30000&maxlen=&min_globalpoint=&max_globalpoint=&minlastup=&maxlastup=&minfirstup=&maxfirstup=&isgl=1&notbl=1&order=new?limit=5',
    parameters: {
        sub: {
            description: 'The target Syosetu subsite.',
            options: Object.entries(SyosetuSub).map(([, value]) => ({
                value,
                label: syosetuSubToJapanese[value],
            })),
        },
        query: 'Search parameters in Syosetu format.',
    },
    features: {
        requireConfig: false,
        requirePuppeteer: false,
        antiCrawler: false,
        supportBT: false,
        supportPodcast: false,
        supportScihub: false,
    },
    name: 'Search',
    maintainers: ['SnowAgar25'],
    handler,
};

const setIfExists = (value) => value ?? undefined;

/**
 * This function converts query string generated by Syosetu website into API-compatible format.
 * It is not intended for users to freely adjust values.
 *
 * @see https://deflis.github.io/node-narou/index.html
 * @see https://dev.syosetu.com/man/api/
 */
function mapToSearchParams(query: string, limit: number): SearchParams {
    const params = queryString.parse(query) as NarouSearchParams;

    const searchParams: SearchParams = {
        gzip: 5,
        lim: limit,
    };

    searchParams.word = setIfExists(params.word);
    searchParams.notword = setIfExists(params.notword);

    searchParams.title = setIfExists(params.title);
    searchParams.ex = setIfExists(params.ex);
    searchParams.keyword = setIfExists(params.keyword);
    searchParams.wname = setIfExists(params.wname);

    searchParams.sasie = setIfExists(params.sasie);
    searchParams.iszankoku = setIfExists(params.iszankoku);
    searchParams.isbl = setIfExists(params.isbl);
    searchParams.isgl = setIfExists(params.isgl);
    searchParams.istensei = setIfExists(params.istensei);
    searchParams.istenni = setIfExists(params.istenni);

    searchParams.stop = setIfExists(params.stop);
    searchParams.notzankoku = setIfExists(params.notzankoku);
    searchParams.notbl = setIfExists(params.notbl);
    searchParams.notgl = setIfExists(params.notgl);
    searchParams.nottensei = setIfExists(params.nottensei);
    searchParams.nottenni = setIfExists(params.nottenni);

    searchParams.minlen = setIfExists(params.minlen);
    searchParams.maxlen = setIfExists(params.maxlen);

    searchParams.type = setIfExists(params.type as NovelTypeParam);
    searchParams.order = setIfExists(params.order as Order);
    searchParams.genre = setIfExists(params.genre as Join<Genre> | Genre);
    searchParams.nocgenre = setIfExists(params.nocgenre as Join<R18Site> | R18Site);

    if (params.mintime || params.maxtime) {
        searchParams.time = `${params.mintime || ''}-${params.maxtime || ''}`;
    }

    return searchParams;
}

const isGeneral = (sub: string): boolean => sub === SyosetuSub.YOMOU;

function createNovelSearchBuilder(sub: string, searchParams: SearchParams) {
    if (isGeneral(sub)) {
        return new SearchBuilder(searchParams, new NarouNovelFetch());
    }

    const r18Params = { ...searchParams };

    switch (sub) {
        case SyosetuSub.NOCTURNE:
            r18Params.nocgenre = R18Site.Nocturne;
            break;
        case SyosetuSub.MOONLIGHT:
            // If either 女性向け/BL is chosen, nocgenre will be in query string
            // If no specific genre selected, include both
            if (!r18Params.nocgenre) {
                r18Params.nocgenre = [R18Site.MoonLight, R18Site.MoonLightBL].join('-') as Join<R18Site>;
            }
            break;
        case SyosetuSub.MIDNIGHT:
            r18Params.nocgenre = R18Site.Midnight;
            break;
        default:
            throw new InvalidParameterError('Invalid Syosetu subsite.\nValid subsites are: yomou, noc, mnlt, mid');
    }

    return new SearchBuilderR18(r18Params, new NarouNovelFetch());
}

async function handler(ctx: Context): Promise<Data> {
    const { sub, query } = ctx.req.param();
    const searchUrl = `https://${sub}.syosetu.com/search/search/search.php?${query}`;

    const limit = Math.min(Number(ctx.req.query('limit') ?? 40), 40);
    const searchParams = mapToSearchParams(query, limit);
    const builder = createNovelSearchBuilder(sub, searchParams);
    const result = await builder.execute();

    const items = result.values.map((novel) => ({
        title: novel.title,
        link: `https://${isGeneral(sub) ? 'ncode' : 'novel18'}.syosetu.com/${String(novel.ncode).toLowerCase()}`,
        description: art(path.join(__dirname, 'templates/description.art'), {
            novel,
            genreText: GenreNotation[novel.genre],
        }),
        // Skip pubDate - search results prioritize search sequence over timestamps
        // pubDate: novel.general_lastup,
        author: novel.writer,
        // Split by whitespace characters(\s), slash(/), full-width slash(／)
        category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean),
    }));

    const searchTerms: string[] = [];
    if (searchParams.word) {
        searchTerms.push(searchParams.word);
    }
    if (searchParams.notword) {
        searchTerms.push(`-${searchParams.notword}`);
    }

    return {
        title: searchTerms.length > 0 ? `Syosetu Search: ${searchTerms.join(' ')}` : 'Syosetu Search',
        link: searchUrl,
        item: items,
    };
}
