import { Route, Data, DataItem } from '@/types';
import { art } from '@/utils/render';
import path from 'node:path';
import { Context } from 'hono';
import { Genre, SearchBuilder, SearchParams, NarouNovelFetch, GenreNotation } from 'narou';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import { handleIsekaiRanking } from './ranking-isekai';
import { RankingPeriod, periodToJapanese, novelTypeToJapanese, periodToOrder, RankingType, NovelType, isekaiCategoryToJapanese, IsekaiCategory } from './types/ranking';

const getParameters = () => {
    // Generate ranking type options
    const rankingTypeOptions = [
        { value: RankingType.LIST, label: '総合ランキング (General Ranking)' },
        { value: RankingType.GENRE, label: 'ジャンル別ランキング (Genre Ranking)' },
        { value: RankingType.ISEKAI, label: '異世界転生/転移ランキング (Isekai Ranking)' },
    ];

    // Generate period options
    const periodOptions = Object.entries(RankingPeriod).map(([key, value]) => ({
        value,
        label: `${periodToJapanese[value]} (${key})`,
    }));

    // Generate novel type options
    const novelTypeOptions = Object.entries(NovelType).map(([key, value]) => ({
        value,
        label: `${novelTypeToJapanese[value]} (${key})`,
    }));

    // Generate genre options
    const genreOptions = Object.entries(Genre)
        .filter(([, value]) => typeof value === 'number') // Filter out reverse mappings
        .map(([key, value]) => ({
            value: value.toString(),
            label: key,
        }));

    // Generate isekai category options
    const isekaiOptions = Object.entries(IsekaiCategory).map(([key, value]) => ({
        value,
        label: `${isekaiCategoryToJapanese[value]} (${key})`,
    }));

    return {
        listType: {
            description: 'Ranking type',
            options: rankingTypeOptions,
        },
        type: {
            description: 'Detailed ranking type, can be found in Syosetu ranking URLs',
            options: [
                // General ranking options
                ...periodOptions.flatMap((period) =>
                    novelTypeOptions.map((novelType) => ({
                        value: `${period.value}_${novelType.value}`,
                        label: `${RankingType.LIST} - [${periodToJapanese[period.value]}] 総合ランキング - ${novelTypeToJapanese[novelType.value]}`,
                    }))
                ),
                // Genre ranking options
                ...periodOptions.flatMap((period) =>
                    genreOptions.flatMap((genre) =>
                        novelTypeOptions.map((novelType) => ({
                            value: `${period.value}_${genre.value}_${novelType.value}`,
                            label: `${RankingType.GENRE} - [${periodToJapanese[period.value]}] ${GenreNotation[genre.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`,
                        }))
                    )
                ),
                // Isekai ranking options
                ...periodOptions.flatMap((period) =>
                    isekaiOptions.flatMap((category) =>
                        novelTypeOptions.map((novelType) => ({
                            value: `${period.value}_${category.value}_${novelType.value}`,
                            label: `${RankingType.ISEKAI} - [${periodToJapanese[period.value]}] 異世界転生/転移${isekaiCategoryToJapanese[category.value]}ランキング - ${novelTypeToJapanese[novelType.value]}`,
                        }))
                    )
                ),
            ],
        },
    };
};

const getBest5RadarItems = () => {
    // List
    const periodRankings = Object.values(RankingPeriod).map((period) => ({
        title: `${periodToJapanese[period]}ランキング BEST5`,
        source: ['yomou.syosetu.com/rank/top/'],
        target: `/ranking/list/${period}_total?limit=5`,
    }));

    // Genre
    const genreRankings = Object.entries(Genre)
        .filter(([, value]) => typeof value === 'number' && value !== Genre.SonotaReplay && value !== Genre.NonGenre)
        .map(([, value]) => ({
            title: `[${periodToJapanese.daily}] ${GenreNotation[value]}ランキング BEST5`,
            source: ['yomou.syosetu.com/rank/top/'],
            target: `/ranking/genre/daily_${value}_total?limit=5`,
        }));

    // Isekai
    const isekaiRankings = Object.values(IsekaiCategory).map((category) => ({
        title: `[${periodToJapanese.daily}] 異世界転生/転移${isekaiCategoryToJapanese[category]}ランキング BEST5`,
        source: ['yomou.syosetu.com/rank/top/'],
        target: `/ranking/isekai/daily_${category}_total?limit=5`,
    }));

    return [...periodRankings, ...genreRankings, ...isekaiRankings];
};

export const route: Route = {
    path: '/ranking/:listType/:type',
    categories: ['reading'],
    example: '/syosetu/ranking/list/daily_total?limit=50',
    parameters: getParameters(),
    features: {
        requireConfig: false,
        requirePuppeteer: false,
        antiCrawler: false,
        supportBT: false,
        supportPodcast: false,
        supportScihub: false,
    },
    name: 'Rankings',
    url: 'yomou.syosetu.com/rank/top',
    maintainers: ['SnowAgar25'],
    handler,
    description: `
| Keyword | Description | 説明 |
| --- | --- | --- |
| list | Overall Ranking | 総合ランキング |
| genre | Genre Ranking | ジャンル別ランキング |
| isekai | Isekai/Reincarnation/Transfer Ranking | 異世界転生/転移ランキング |

| Period | Description |
| --- | --- |
| daily | Daily Ranking |
| weekly | Weekly Ranking |
| monthly | Monthly Ranking |
| quarter | Quarterly Ranking |
| yearly | Yearly Ranking |


| Type | Description |
| --- | --- |
| total | All Works |
| t | Short Stories |
| r | Ongoing Series |
| er | Completed Series |

::: warning
Please note that novel type options may vary depending on the ranking category.

ランキングの種類によって、小説タイプが異なる場合がございますのでご注意ください。
:::

::: danger 注意事項
The "注目度ランキング" (Attention Ranking) is not supported as syosetu does not provide a public API for this feature and the results cannot be replicated through the search API.

「注目度ランキング」については、API が非公開で検索 API でも同様の結果を得ることができないため、本 Route ではサポートしておりません。
:::

::: tip 異世界転生/転移ランキングについて (Isekai)
When multiple works have the same points, their order may differ from syosetu's ranking as syosetu randomizes the order for works with identical points.

集計の結果、同じポイントの作品が複数存在する場合、Syosetu ではランダムで順位が決定されるため、本 Route の順位と異なる場合があります。
:::
`,
    radar: [
        {
            source: ['yomou.syosetu.com/rank/list/type/:type'],
            target: '/ranking/list/:type',
        },
        {
            source: ['yomou.syosetu.com/rank/genrelist/type/:type'],
            target: '/ranking/genre/:type',
        },
        {
            source: ['yomou.syosetu.com/rank/isekailist/type/:type'],
            target: '/ranking/isekai/:type',
        },
        ...getBest5RadarItems(),
    ],
};

function parseGeneralRankingType(type: string): { period: RankingPeriod; novelType: NovelType } {
    const [periodStr, novelTypeStr] = type.split('_');

    const period = periodStr as RankingPeriod;
    const novelType = novelTypeStr as NovelType;

    const isValid = [Object.values(RankingPeriod).includes(period), Object.values(NovelType).includes(novelType)].every(Boolean);

    if (!isValid) {
        throw new InvalidParameterError(`Invalid general ranking type: ${type}`);
    }

    return { period, novelType };
}

function parseGenreRankingType(type: string): { period: RankingPeriod; genre: number; novelType: NovelType } {
    const [periodStr, genreStr, novelTypeStr = NovelType.TOTAL] = type.split('_');

    const period = periodStr as RankingPeriod;
    const genre = Number(genreStr) as Genre;
    const novelType = novelTypeStr as NovelType;

    const isValid = [Object.values(RankingPeriod).includes(period), Object.values(Genre).includes(genre), Object.values(NovelType).includes(novelType), genre !== Genre.SonotaReplay, genre !== Genre.NonGenre].every(Boolean);

    if (!isValid) {
        throw new InvalidParameterError(`Invalid genre ranking type: ${type}`);
    }

    return { period, genre, novelType };
}

async function handler(ctx: Context): Promise<Data> {
    const { listType, type } = ctx.req.param();
    const rankingType = listType as RankingType;
    const limit = Math.min(Number(ctx.req.query('limit') ?? 300), 300);

    const api = new NarouNovelFetch();
    const searchParams: SearchParams = {
        gzip: 5,
        lim: limit,
    };

    let rankingUrl: string;
    let rankingTitle: string;

    // Build search parameters and titles based on ranking type
    switch (rankingType) {
        case RankingType.LIST: {
            const { period, novelType } = parseGeneralRankingType(type);
            rankingUrl = `https://yomou.syosetu.com/rank/list/type/${type}`;
            rankingTitle = `[${periodToJapanese[period]}] 総合ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`;

            searchParams.order = periodToOrder[period];
            if (novelType !== NovelType.TOTAL) {
                searchParams.type = novelType;
            }
            break;
        }

        case RankingType.GENRE: {
            const { period, genre, novelType } = parseGenreRankingType(type);
            rankingUrl = `https://yomou.syosetu.com/rank/genrelist/type/${type}`;
            rankingTitle = `[${periodToJapanese[period]}] ${GenreNotation[genre]}ランキング - ${novelTypeToJapanese[novelType]} BEST${limit}`;

            searchParams.order = periodToOrder[period];
            searchParams.genre = genre as Genre;
            if (novelType !== NovelType.TOTAL) {
                searchParams.type = novelType;
            }
            break;
        }

        case RankingType.ISEKAI:
            return handleIsekaiRanking(type, limit);

        default:
            throw new InvalidParameterError(`Invalid ranking type: ${type}`);
    }

    const builder = new SearchBuilder(searchParams, api);
    const result = await builder.execute();

    const items = result.values.map((novel, index) => ({
        title: `#${index + 1} ${novel.title}`,
        link: `https://ncode.syosetu.com/${String(novel.ncode).toLowerCase()}`,
        description: art(path.join(__dirname, 'templates/description.art'), {
            novel,
        }),
        author: novel.writer,
        category: novel.keyword.split(/[\s/\uFF0F]/).filter(Boolean),
    }));

    return {
        title: `小説家になろう - ${rankingTitle}`,
        link: rankingUrl,
        item: items as DataItem[],
        language: 'ja',
    };
}
