import { CustomPostDefinition, Definition, Empty, JsonStatus, JsonWrappedComment, LinkedBundle, Listing, SubmitResponse, UIEventHandlerDefinition, } from '@devvit/protos';
import { Header } from '@devvit/shared-types/Header.js';
import { assertNonNull } from '@devvit/shared-types/NonNull.js';
import { RichTextBuilder } from '@devvit/shared-types/richtext/RichTextBuilder.js';
import { isCommentId } from '@devvit/shared-types/tid.js';
import { fromByteArray } from 'base64-js';
import { Buffer } from '../../lib/Buffer.js';
import { RedditAPI } from './RedditAPI.js';
import { findAndThrowRedditAPIError } from './errors.js';
import { deepSnakeToCamel } from './snakeToCamel.js';
const customPostTemplate = (encodedBundle, encodedConfig, encodedCached) => new RichTextBuilder()
    .heading({ level: 1 }, (h) => {
    h.rawText('DX_Bundle:');
})
    .codeBlock({}, (c) => {
    c.rawText(encodedBundle);
})
    .heading({ level: 1 }, (h) => {
    h.rawText('DX_Config:');
})
    .codeBlock({}, (c) => {
    c.rawText(encodedConfig);
})
    .heading({ level: 1 }, (h) => {
    h.rawText('DX_Cached:');
})
    .codeBlock({}, (c) => {
    c.rawText(encodedCached);
})
    .build();
export class LinksAndComments extends RedditAPI {
    async Comment(request, metadata) {
        const response = await this._fetch('Comment', request, metadata);
        // It's imperative that `response.body` is wrapped in a `Buffer.from`
        let body = JSON.parse(new TextDecoder().decode(Buffer.from(response.body)));
        findAndThrowRedditAPIError(response.status, body);
        // When richtextJson is provided the response comes back unwrapped.
        if (!body.json && request.richtextJson && isCommentId(body.name)) {
            body = {
                json: {
                    errors: [],
                    data: {
                        things: [
                            {
                                kind: 't1',
                                data: body,
                            },
                        ],
                    },
                },
            };
        }
        return JsonWrappedComment.fromJSON(deepSnakeToCamel(body));
    }
    async Del(request, metadata) {
        await this._fetch('Del', request, metadata);
        return Empty.fromPartial({});
    }
    async EditUserText(request, metadata) {
        const response = await this._fetch('EditUserText', request, metadata);
        return JsonWrappedComment.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async FollowPost(request, metadata) {
        await this._fetch('FollowPost', request, metadata);
        return Empty.fromPartial({});
    }
    async Hide(request, metadata) {
        await this._fetch('Hide', request, metadata);
        return Empty.fromPartial({});
    }
    async Info(request, metadata) {
        const response = await this._fetch('Info', request, metadata);
        return Listing.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async Lock(request, metadata) {
        await this._fetch('Lock', request, metadata);
        return Empty.fromPartial({});
    }
    async MarkNSFW(request, metadata) {
        await this._fetch('MarkNSFW', request, metadata);
        return Empty.fromPartial({});
    }
    async MoreChildren(request, metadata) {
        const response = await this._fetch('MoreChildren', request, metadata);
        return JsonWrappedComment.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async Report(request, metadata) {
        const response = await this._fetch('Report', request, metadata);
        return JsonStatus.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async ReportAward(request, metadata) {
        await this._fetch('ReportAward', request, metadata);
        return Empty.fromPartial({});
    }
    async Save(request, metadata) {
        await this._fetch('Save', request, metadata);
        return Empty.fromPartial({});
    }
    async SendReplies(request, metadata) {
        await this._fetch('SendReplies', request, metadata);
        return Empty.fromPartial({});
    }
    async SetContestMode(request, metadata) {
        const response = await this._fetch('SetContestMode', request, metadata);
        return JsonStatus.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async SetSubredditSticky(request, metadata) {
        const response = await this._fetch('SetSubredditSticky', request, metadata);
        return JsonStatus.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async SetSuggestedSort(request, metadata) {
        const response = await this._fetch('SetSuggestedSort', request, metadata);
        return JsonStatus.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async Spoiler(request, metadata) {
        await this._fetch('Spoiler', request, metadata);
        return Empty.fromPartial({});
    }
    async Submit(request, metadata) {
        if (request.kind === 'custom') {
            return this.SubmitCustomPost(request, metadata);
        }
        const subredditName = await this._getSubredditNameFromMetadata(metadata);
        if (request.sr !== subredditName && request.kind !== 'crosspost') {
            throw new Error(`The app is not allowed to post to "r/${request.sr}"`);
        }
        const response = await this._fetch('Submit', request, metadata);
        return SubmitResponse.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async SubmitCustomPost(request, metadata) {
        const hostname = metadata[Header.RemoteHostname]?.values[0];
        assertNonNull(hostname, 'Remote hostname is missing');
        const subredditName = await this._getSubredditNameFromMetadata(metadata);
        if (request.sr !== subredditName) {
            throw new Error(`The app is not allowed to post to "r/${request.sr}"`);
        }
        const buildInfo = { dependencies: {} };
        const deps = metadata[Header.AppDependencies]?.values ?? [];
        buildInfo.dependencies = deps.reduce((dependencies, entry) => {
            const tokens = entry.split('=');
            dependencies[tokens[0]] = tokens[1];
            return dependencies;
        }, buildInfo.dependencies);
        // to-do: this is kind of _another_ Linker implementation and introduces a
        //        new subset of LinkedBundle. Try to consolidate with SkinnyLinker.
        const bundle = LinkedBundle.fromPartial({
            hostname,
            provides: [CustomPostDefinition, UIEventHandlerDefinition].map((d) => Definition.toSerializable(d)),
            ...(deps.length > 0 ? { buildInfo } : {}),
        });
        // Fanghorn feeds expect richtextJson, so replace text with richtextJson if it's set.
        if (!request.richtextJson && request.text)
            request.richtextJson = request.text;
        // request.text is deleted as both text and richtext_json may not be present.
        if (request.richtextJson)
            delete request.text;
        assertNonNull(request.richtextJson, 'Custom post blocks is missing. richtextJson field must be set.');
        const encodedBundle = fromByteArray(LinkedBundle.encode(bundle).finish());
        const encodedCached = request.richtextJson;
        request.richtextJson = customPostTemplate(encodedBundle, 'EgA=', encodedCached);
        request.kind = 'self';
        const response = await this._fetch('Submit', request, metadata);
        return SubmitResponse.fromJSON(this._decodeAndValidateResponseJSON(response));
    }
    async Unhide(request, metadata) {
        await this._fetch('Unhide', request, metadata);
        return Empty.fromPartial({});
    }
    async Unlock(request, metadata) {
        await this._fetch('Unlock', request, metadata);
        return Empty.fromPartial({});
    }
    async UnmarkNSFW(request, metadata) {
        await this._fetch('UnmarkNSFW', request, metadata);
        return Empty.fromPartial({});
    }
    async Unsave(request, metadata) {
        await this._fetch('Unsave', request, metadata);
        return Empty.fromPartial({});
    }
    async Unspoiler(request, metadata) {
        await this._fetch('Unspoiler', request, metadata);
        return Empty.fromPartial({});
    }
    async Vote(request, metadata) {
        await this._fetch('Vote', request, metadata);
        return Empty.fromPartial({});
    }
    async _getSubredditNameFromMetadata(metadata) {
        const appSubredditId = metadata?.[Header.Subreddit]?.values?.[0];
        assertNonNull(appSubredditId, 'No Subreddit ID in Metadata');
        const appSubredditResponse = await this.Info({ thingIds: [appSubredditId], subreddits: [] }, metadata);
        const subredditName = appSubredditResponse.data?.children[0]?.data?.displayName;
        assertNonNull(subredditName, 'Subreddit not found');
        return subredditName;
    }
}
