import { decode, encode, authenticate, bytes32Hex, membershipVerifierFactory, ClubsPluginCategory, SinglePath, fetchProfile, ClubsSlotName } from '@devprotocol/clubs-core';
import { randomBytes, recoverAddress, hashMessage, getDefaultProvider, verifyMessage, JsonRpcProvider, ZeroAddress } from 'ethers';
import { v5 } from 'uuid';
import { SchemaFieldTypes, createClient } from 'redis';
import { whenNotError, whenDefined, whenDefinedAll, whenNotErrorAll } from '@devprotocol/util-ts';
import { reduceBy, aperture, tryCatch } from 'ramda';
import Screenshot1 from '../src/assets/images/posts-1.jpg';
import Screenshot2 from '../src/assets/images/posts-2.jpg';
import Screenshot3 from '../src/assets/images/posts-3.jpg';
import Icon from '../src/assets/images/plugin-icon.svg';
import { CreateNavigationLink } from '@devprotocol/clubs-core/layouts';
import Feed from '../src/pages/Feed.astro';
import ListFeed from '../src/pages/ListFeed.astro';
import NavigationLink from '../src/slots/NavigationLink.astro';
import Posts_ from '../src/pages/Posts.astro';
import Readme from '../src/readme.astro';
import News from '../src/pages/News.astro';

const uuidFactory = (url) => {
    const namespase = v5(url, v5.URL);
    return () => v5(randomBytes(32), namespase);
};

var Index;
(function (Index) {
    Index["Post"] = "idx::devprotocol:clubs:plugin:posts::post";
    Index["Comment"] = "idx::devprotocol:clubs:plugin:posts::comment";
    Index["Reaction"] = "idx::devprotocol:clubs:plugin:posts::reaction";
    Index["Option"] = "idx::devprotocol:clubs:plugin:posts::option";
})(Index || (Index = {}));
var Prefix;
(function (Prefix) {
    Prefix["Post"] = "doc::devprotocol:clubs:plugin:posts::post:";
    Prefix["Comment"] = "doc::devprotocol:clubs:plugin:posts::comment:";
    Prefix["Reaction"] = "doc::devprotocol:clubs:plugin:posts::reaction:";
    Prefix["Option"] = "doc::devprotocol:clubs:plugin:posts::option:";
})(Prefix || (Prefix = {}));
const _raw = {
    '$._raw': {
        type: SchemaFieldTypes.TEXT,
        AS: '_raw',
    },
};
const _scope = {
    '$._scope': {
        type: SchemaFieldTypes.TAG,
        AS: '_scope',
    },
};
const _post_id = {
    '$._post_id': {
        type: SchemaFieldTypes.TAG,
        AS: '_post_id',
    },
};
const _parent_type = {
    '$._parent_type': {
        type: SchemaFieldTypes.TEXT,
        AS: '_parent_type',
    },
};
const _parent_id = {
    '$._parent_id': {
        type: SchemaFieldTypes.TAG,
        AS: '_parent_id',
    },
};
const id = {
    '$.id': {
        type: SchemaFieldTypes.TAG,
        AS: 'id',
    },
};
const title = {
    '$.title': {
        type: SchemaFieldTypes.TEXT,
        AS: 'title',
    },
};
const content = {
    '$.content': {
        type: SchemaFieldTypes.TEXT,
        AS: 'content',
    },
};
const created_by = {
    '$.created_by': {
        type: SchemaFieldTypes.TEXT,
        AS: 'created_by',
    },
};
const created_at = {
    '$.created_at': {
        type: SchemaFieldTypes.NUMERIC,
        SORTABLE: true,
        AS: 'created_at',
    },
};
const updated_at = {
    '$.updated_at': {
        type: SchemaFieldTypes.NUMERIC,
        SORTABLE: true,
        AS: 'updated_at',
    },
};
const key = {
    '$.key': {
        type: SchemaFieldTypes.TEXT,
        AS: 'key',
    },
};
const value = {
    '$.value': {
        type: SchemaFieldTypes.TEXT,
        AS: 'value',
    },
};

const uuidToQuery = (id) => id.replaceAll('-', '\\-');

/* eslint-disable functional/no-expression-statement */
const defaultClient = () => createClient({
    url: import.meta.env.REDIS_URL,
    username: import.meta.env.REDIS_USERNAME ?? '',
    password: import.meta.env.REDIS_PASSWORD ?? '',
    socket: {
        keepAlive: 1,
        reconnectStrategy: 1,
    },
});
const getDefaultClient = async () => {
    const client = defaultClient();
    if (client.isOpen === false) {
        await client.connect();
    }
    return client;
};
/**
 * Fetches the paginated posts from Redis
 * @param scope - The scope of the posts
 * @param client - The Redis client
 * @param page - The page number
 * @returns The paginated posts
 */
const getPaginatedPosts = async ({ scope, client, page, }) => {
    const limit = 10;
    const fetchPosts = await client.ft.search(Index.Post, `@_scope:{${uuidToQuery(scope)}}`, {
        LIMIT: { from: page, size: limit },
        SORTBY: {
            BY: 'created_at',
            DIRECTION: 'DESC',
        },
    });
    const postIds = fetchPosts.documents
        // JSON parse each post
        .map((post) => post.value)
        // filter out null values from the array
        .filter((post) => post !== null)
        // decode post data (get post data without document db related keys)
        .map(({ id }) => id);
    const results = await Promise.all(postIds.map((id) => fetchSinglePost({ id, client })));
    return results;
};
/**
 * Fetches the single post from Redis
 * @param id - The id of the post
 * @param client - The Redis client
 * @returns The post
 */
const fetchSinglePost = async ({ id, client, }) => {
    const fetchPost = await client.ft.search(Index.Post, `@id:{${uuidToQuery(id)}}`, {
        LIMIT: { from: 0, size: 1 },
    });
    const posts = fetchPost.documents
        // JSON parse each post
        .map((post) => post.value)
        // filter out null values from the array
        .filter((post) => post !== null)
        // decode post data (get post data without document db related keys)
        .map(({ _raw, ...all }) => ({
        source: decode(_raw),
        _raw,
        ...all,
    }));
    /**
     * Create fetch promises for each post
     */
    const fetchCommentsPromises = posts.map(async (post) => {
        const scope = post._scope;
        const comments = await fetchComments({ scope, postId: post.id, client });
        const options = await Promise.all(comments.map((comment) => fetchAllOptions({
            scope,
            parentType: 'comment',
            parentId: comment.id,
            client,
        })));
        const commentsWithOpts = comments.map((comment, i) => ({
            ...comment,
            options: options[i],
        }));
        // decode each comment data (get comment data without document db related keys)
        return commentsWithOpts.map(({ _raw, options }) => ({
            ...decode(_raw),
            options: options.map(({ _raw: _rawOpt }) => decode(_rawOpt)),
        }));
    });
    /**
     * Create fetch options promises for each post
     */
    const fetchOptionsPromises = posts.map(async (post) => {
        const scope = post._scope;
        const options = await fetchAllOptions({
            scope,
            parentType: 'post',
            parentId: post.id,
            client,
        });
        // decode each option data
        return options.map(({ _raw }) => decode(_raw));
    });
    /**
     * Create fetch reactions promises for each post
     */
    const fetchReactionsPromises = posts.map(async (post) => {
        const scope = post._scope;
        const reactions = await fetchAllReactions({
            scope,
            postId: post.id,
            client,
        });
        const groupCreatedBy = (acc, { created_by }) => acc.concat(created_by);
        const toEmoji = ({ content }) => content;
        return reduceBy(groupCreatedBy, [], toEmoji, reactions);
    });
    /**
     * Fetch all comments/options/reactoins for each post
     */
    const [postsComments, postsOptions, postsReactions] = await Promise.all([
        Promise.all(fetchCommentsPromises),
        Promise.all(fetchOptionsPromises),
        Promise.all(fetchReactionsPromises),
    ]);
    /**
     * Map the comments to the posts
     */
    const [result] = posts.map((post, index) => ({
        ...post.source,
        comments: postsComments[index],
        options: postsOptions[index],
        reactions: postsReactions[index],
    }));
    return result;
};

/* eslint-disable functional/no-expression-statement */
const postDocument = ({ data, scope, }) => ({
    id: data.id,
    title: data.title,
    content: data.content,
    created_by: data.created_by,
    created_at: data.created_at.getTime(),
    updated_at: data.updated_at.getTime(),
    _raw: encode(data),
    _scope: scope,
});
const commentDocument = ({ data, scope, postId, }) => ({
    id: data.id,
    content: data.content,
    created_by: data.created_by,
    created_at: data.created_at.getTime(),
    updated_at: data.updated_at.getTime(),
    _raw: encode(data),
    _scope: scope,
    _post_id: postId,
});
const reactionDocument = ({ data, scope, postId, }) => ({
    content: data.content,
    created_by: data.created_by,
    _scope: scope,
    _post_id: postId,
});
const optionDocument = ({ data, scope, parentType, parentId, }) => ({
    key: data.key,
    value: JSON.stringify(data.value),
    _scope: scope,
    _raw: encode(data),
    _parent_type: parentType,
    _parent_id: parentId,
});
const generatePrefixOf = (prefix, scope) => `${prefix}:${scope}`;
const generateKeyOf = (prefix, scope, id) => `${generatePrefixOf(prefix, scope)}:${id}`;
const createOrSkip = async (name, prefix, scm) => {
    const client = await getDefaultClient();
    return client.ft
        .create(name, scm, {
        ON: 'JSON',
        PREFIX: prefix,
    })
        .then((result) => ({ result, name }))
        .catch((err) => ({ result: err.message, name }));
};
const createIndex = async (client) => {
    const schemaPost = {
        ...id,
        ...title,
        ...content,
        ...created_by,
        ...created_at,
        ...updated_at,
        ..._raw,
        ..._scope,
    };
    const schemaComment = {
        ...id,
        ...content,
        ...created_by,
        ...created_at,
        ...updated_at,
        ..._raw,
        ..._scope,
        ..._post_id,
    };
    const schemaReaction = {
        ...content,
        ...created_by,
        ..._scope,
        ..._post_id,
    };
    const schemaOption = {
        ...key,
        ...value,
        ..._raw,
        ..._parent_type,
        ..._parent_id,
        ..._scope,
    };
    const res = await Promise.all([
        createOrSkip(Index.Post, Prefix.Post, schemaPost),
        createOrSkip(Index.Comment, Prefix.Comment, schemaComment),
        createOrSkip(Index.Reaction, Prefix.Reaction, schemaReaction),
        createOrSkip(Index.Option, Prefix.Option, schemaOption),
    ]);
    return res;
};
const setPost = async ({ scope, post, url, client, }) => {
    const uuid = uuidFactory(url);
    const _post = {
        id: post.id,
        title: post.title,
        content: post.content,
        created_by: post.created_by,
        created_at: post.created_at,
        updated_at: post.updated_at,
    };
    const newPostData = postDocument({ data: _post, scope });
    const newReactionsData = Object.keys(post.reactions)
        .map((key) => {
        const users = post.reactions[key];
        return users.map((created_by) => reactionDocument({
            data: {
                content: key,
                created_by,
            },
            scope: scope,
            postId: post.id,
        }));
    })
        .flat();
    const newPostOptionsData = post.options.map((data) => optionDocument({
        data: {
            key: data.key,
            value: data.value,
        },
        scope,
        parentType: 'post',
        parentId: post.id,
    }));
    await client.json.set(generateKeyOf(Prefix.Post, scope, _post.id), '$', newPostData);
    await Promise.all(post.comments.map((data) => setComment({
        scope,
        url,
        comment: data,
        postId: post.id,
        client,
    })));
    await Promise.all(newReactionsData.map((data) => client.json.set(generateKeyOf(Prefix.Reaction, scope, uuid()), '$', data)));
    await Promise.all(newPostOptionsData.map((data) => client.json.set(generateKeyOf(Prefix.Option, scope, uuid()), '$', data)));
    return true;
};
const deletePost = async ({ postId, client, scope, userAddress, }) => {
    // first fetch the post
    const doc = await client.json.get(`${Prefix.Post}:${scope}:${postId}`);
    if (!doc) {
        return false;
    }
    const post = doc?.valueOf();
    // ensure the user is the owner of the post
    if (post.created_by !== userAddress) {
        return false;
    }
    // delete by id
    const success = await client.del(`${Prefix.Post}:${scope}:${postId}`);
    // return success === 1
    // @todo: delete comments
    // @todo: delete reactions
    const deleteAllComments = async (scope, postId, client) => {
        // eslint-disable-next-line functional/no-let
        let page = 0;
        // eslint-disable-next-line functional/no-let
        let comments;
        // eslint-disable-next-line functional/no-loop-statement
        do {
            comments = await fetchComments({ scope, postId, client, page });
            const deleteCommentPromises = comments.map((comment) => deleteComment({
                scope,
                commentId: comment.id,
                parentType: 'post',
                client,
            }));
            await Promise.all(deleteCommentPromises);
            page++;
        } while (comments.length > 0);
        return true;
    };
    const commentsDeleted = deleteAllComments(scope, postId, client);
    const reactions = await fetchAllReactions({ scope, postId, client });
    // delete all reactions with postId
    const deleteReactionsPromises = reactions.map((reaction) => {
        return client.del(reaction.id);
    });
    await Promise.all(deleteReactionsPromises);
    return success === 1 && commentsDeleted;
};
const setReaction = async ({ scope, reaction, postId, url, client, createdBy, }) => {
    await implSetReaction({ scope, reaction, postId, url, client, createdBy });
    return true;
};
const implSetReaction = async ({ scope, reaction, postId, url, client, createdBy, }) => {
    const uuid = uuidFactory(url);
    const newReactionData = reactionDocument({
        data: {
            content: reaction,
            created_by: createdBy,
        },
        scope: scope,
        postId,
    });
    await client.json.set(generateKeyOf(Prefix.Reaction, scope, uuid()), '$', newReactionData);
};
const setComment = async ({ scope, comment, postId, url, client, }) => {
    const uuid = uuidFactory(url);
    const { options, ..._comment } = comment;
    const newCommentData = commentDocument({
        data: _comment,
        scope,
        postId,
    });
    const newCommentOptionsData = options.map((opt) => optionDocument({
        data: {
            key: opt.key,
            value: opt.value,
        },
        scope,
        parentType: 'comment',
        parentId: comment.id,
    }));
    await client.json.set(generateKeyOf(Prefix.Comment, scope, comment.id), '$', newCommentData);
    await Promise.all(newCommentOptionsData.map((data) => client.json.set(generateKeyOf(Prefix.Option, scope, uuid()), '$', data)));
    return true;
};
const deleteComment = async ({ scope, parentType, commentId, client, }) => {
    const query = `@${_parent_type['$._parent_type'].AS}:${parentType} @${_parent_id['$._parent_id'].AS}:{${uuidToQuery(commentId)}}`;
    const commentOptionsDataRecord = await client.ft.search(Index.Option, query);
    const commentKey = generateKeyOf(Prefix.Comment, scope, commentId);
    try {
        await client.del(commentKey); // Delete the comment.
        await Promise.all(commentOptionsDataRecord.documents.map(async (record) => await client.del(record.id)));
        return true;
    }
    catch (err) {
        return false;
    }
};
/**
 * Fetch paginated comments related to the parent post
 * @param scope - the associated db scope
 * @param postId - the parent post id
 * @param client - the redis client
 * @param page - the page number
 * @returns the comments
 */
const fetchComments = async ({ scope, postId, client, page, }) => {
    // eslint-disable-next-line no-param-reassign
    page = page || 1;
    const limit = 10;
    const start = (page - 1) * limit;
    /**
     * Search comments where parent id is postId
     */
    const result = await client.ft.search(Index.Comment, `@${_post_id['$._post_id'].AS}:{${uuidToQuery(postId)}}`, {
        LIMIT: {
            from: start,
            size: limit,
        },
    });
    /**
     * Convert the result to CommentDocument[]
     */
    const comments = result.documents.map((doc) => doc.value);
    return comments;
};
/**
 * Fetch all options related to the parent post/comment
 * @param scope - the associated db scope
 * @param parentId - the parent post/comment id
 * @param parentType - post or comment
 * @param client - the redis client
 * @param page - the page number
 * @returns the comments
 */
const fetchAllOptions = async ({ scope, parentId, parentType, client, }) => {
    const limit = 10;
    /**
     * Search options where parent id is parentId
     */
    const query = `@${_parent_type['$._parent_type'].AS}:${parentType} @${_parent_id['$._parent_id'].AS}:{${uuidToQuery(parentId)}}`;
    const loop = async (start, list) => {
        const result = await client.ft.search(Index.Option, query, {
            LIMIT: {
                from: start,
                size: limit,
            },
        });
        const docs = [
            ...list,
            ...result.documents.map(({ value }) => value),
        ];
        const isAll = result.total <= start + limit;
        return isAll ? docs : await loop(start + limit + 1, docs);
    };
    const result = await loop(0, []);
    return result;
};
/**
 * Fetch all reactions related to the parent post
 * @param scope - the associated db scope
 * @param postId - the parent post id
 * @param client - the redis client
 * @param page - the page number
 * @returns the comments
 */
const fetchAllReactions = async ({ scope, postId, client, }) => {
    const limit = 10;
    /**
     * Search options where parent id is parentId
     */
    const query = `@${_post_id['$._post_id'].AS}:{${uuidToQuery(postId)}}`;
    const loop = async (start, list) => {
        const result = await client.ft.search(Index.Reaction, query, {
            LIMIT: {
                from: start,
                size: limit,
            },
        });
        const docs = [
            ...list,
            ...result.documents.map(({ value, id }) => {
                return { ...value, id };
            }),
        ];
        const isAll = result.total <= start + limit;
        return isAll ? docs : await loop(start + limit + 1, docs);
    };
    const result = await loop(0, []);
    return result;
};

const addCommentDocumentsRedis = async ({ conf, data, dbQueryKey, postId, }) => {
    try {
        const client = await getDefaultClient();
        const saved = await setComment({
            scope: dbQueryKey,
            comment: data,
            postId,
            url: conf.url,
            client,
        });
        return saved
            ? new Response(JSON.stringify({
                message: saved,
                data: encode(data),
            }), {
                status: 200,
            })
            : new Response(JSON.stringify({
                error: 'Some data is missing',
                data: null,
            }), {
                status: 400,
            });
    }
    catch (e) {
        return new Response(JSON.stringify({
            error: e?.message,
            data: null,
        }), {
            status: 500,
        });
    }
};
const deleteCommentDocumentsRedis = async ({ conf, commentId, dbQueryKey, postId, }) => {
    try {
        const client = await getDefaultClient();
        const deleted = await deleteComment({
            scope: dbQueryKey,
            commentId: commentId,
            parentType: 'comment',
            client,
        });
        return deleted
            ? new Response(JSON.stringify({
                message: deleted,
            }), {
                status: 200,
            })
            : new Response(JSON.stringify({
                error: 'Some data is missing',
                data: null,
            }), {
                status: 400,
            });
    }
    catch (e) {
        return new Response(JSON.stringify({
            error: e?.message,
            data: null,
        }), {
            status: 500,
        });
    }
};

const canDeleteComment = async (config, postOwner, commentOwner, hash, sig) => {
    // Get who is sending request.
    const requestSenderAddress = recoverAddress(hashMessage(hash), sig);
    // Comment owner can delete.
    if (commentOwner === requestSenderAddress) {
        return true;
    }
    // Post owner can delete.
    if (postOwner === requestSenderAddress) {
        return true;
    }
    // Club admin.
    const isAuthenticated = await authenticate({
        message: hash,
        signature: sig,
        previousConfiguration: encode(config),
        provider: getDefaultProvider(config.rpcUrl),
    });
    if (isAuthenticated) {
        return true;
    }
    return false;
};

const addCommentHandler = (conf, dbQueryKey) => async ({ request }) => {
    const { contents, hash, sig, postId } = (await request.json());
    if (!contents || !hash || !sig || !postId) {
        return new Response(JSON.stringify({
            error: 'Missing data',
        }), {
            status: 400,
        });
    }
    // === COMMENTS ===
    const date = new Date();
    const decodedContents = decode(contents);
    const newComment = {
        ...decodedContents,
        id: uuidFactory(conf.url)(),
        created_by: verifyMessage(hash, sig),
        created_at: date,
        updated_at: date,
    };
    return addCommentDocumentsRedis({
        conf,
        data: newComment,
        postId,
        dbQueryKey,
    });
};
const deleteCommentHandler = (conf, dbQueryKey) => async ({ request }) => {
    const { commentId, hash, sig, postId } = (await request.json());
    if (!commentId || !hash || !sig || !postId) {
        return new Response(JSON.stringify({
            error: 'Missing data',
        }), {
            status: 400,
        });
    }
    const client = await getDefaultClient();
    const postKey = generateKeyOf(Prefix.Post, dbQueryKey, postId);
    const post = (await client.json.get(postKey));
    const commentKey = generateKeyOf(Prefix.Comment, dbQueryKey, commentId);
    const comment = (await client.json.get(commentKey));
    if (!comment || !post) {
        return new Response(// Comment not found.
        JSON.stringify({
            error: 'Missing comment or post',
            data: null,
        }), {
            status: 400,
        });
    }
    if (comment._post_id !== postId) {
        return new Response(// CommentId and PostId not linked...
        JSON.stringify({
            error: 'Post not linked with comment',
            data: null,
        }), {
            status: 400,
        });
    }
    const validationResult = await canDeleteComment(conf, post.created_by, comment.created_by, hash, sig);
    if (!validationResult) {
        return new Response(// CommentId and PostId not linked...
        JSON.stringify({
            error: 'Bad access',
            data: null,
        }), {
            status: 400,
        });
    }
    return deleteCommentDocumentsRedis({
        conf,
        commentId,
        postId,
        dbQueryKey,
    });
};
/**
 * This can be used to fetch more paginated post comments by page id
 * @param conf
 * @param dbQueryKey
 * @returns paginated comments
 */
const fetchCommentsHandler = (conf) => async ({ request, url, }) => {
    const client = await getDefaultClient();
    /** get the parent post id */
    const splitUrl = url.pathname.split('/');
    const postId = splitUrl[splitUrl.length - 2];
    /** page page number from query params */
    const page = url.searchParams.get('page');
    try {
        /**
         * fetch the comments
         */
        const comments = await fetchComments({
            scope: conf.url,
            postId,
            client,
            page: page ? parseInt(page) : 0,
        });
        return new Response(JSON.stringify({
            comments,
        }), {
            status: 200,
        });
    }
    catch (e) {
        return new Response(JSON.stringify({
            error: e,
            data: null,
        }), {
            status: 500,
        });
    }
};

const filterRequiredMemberships = ({ post, memberships, }) => {
    const requiredMemberships = post.options?.find(({ key }) => key === 'require-one-of')
        ?.value ?? [];
    return requiredMemberships
        .map((key) => memberships?.find((mem) => bytes32Hex(mem.payload ?? []) === bytes32Hex(key)) ?? [])
        .flat();
};
const hasWritePermission = async ({ account, provider, propertyAddress, memberships = [], roles, }) => {
    const membershipVerifier = await membershipVerifierFactory({
        provider,
        propertyAddress,
        account,
    }).catch((err) => err);
    const requireMemberships = roles?.write.memberships
        .map((payload) => {
        return memberships
            ?.filter((membership) => bytes32Hex(membership.payload ?? []) === bytes32Hex(payload))
            .flat();
    })
        .flat()
        .filter((x) => x !== undefined);
    const verify = requireMemberships
        ? await whenNotError(membershipVerifier, (verifier) => verifier(requireMemberships))
        : { result: false };
    // eslint-disable-next-line functional/no-expression-statement
    console.log({ verify });
    return verify instanceof Error ? false : verify.result;
};

const maskOptions = (options) => {
    return options
        .map((opt) => (opt.key.startsWith('#') ? undefined : opt))
        .filter((x) => x !== undefined);
};
const mask = (post) => {
    return {
        ...post,
        content: '',
        options: maskOptions(post.options),
        comments: post.comments.map((comment) => ({
            ...comment,
            options: maskOptions(comment.options),
        })),
        masked: true,
    };
};
const maskFactory = async ({ user, propertyAddress, rpcUrl, memberships, }) => {
    const provider = new JsonRpcProvider(rpcUrl);
    const membershipVerifier = await whenDefined(user, (account) => membershipVerifierFactory({
        provider,
        propertyAddress,
        account,
    }));
    return async (post) => {
        // Do not Mask : post.created_by === user
        if (post.created_by === user) {
            return post;
        }
        const requiredMemberships = filterRequiredMemberships({
            post,
            memberships: [...memberships],
        });
        const hasRequiredMemberships = requiredMemberships.length > 0;
        const verified = membershipVerifier && hasRequiredMemberships
            ? (await membershipVerifier(requiredMemberships)).result
            : !hasRequiredMemberships;
        return verified ? post : mask(post);
    };
};

const addReactionDocumentsRedis = async ({ conf, data, postId, userAddress, dbQueryKey, }) => {
    try {
        const client = await getDefaultClient();
        const saved = await setReaction({
            scope: dbQueryKey,
            reaction: data,
            postId,
            url: conf.url,
            client,
            createdBy: userAddress,
        });
        return saved
            ? new Response(JSON.stringify({
                message: saved,
                data: encode(data),
            }), {
                status: 200,
            })
            : new Response(JSON.stringify({
                error: 'Some data is missing',
                data: null,
            }), {
                status: 400,
            });
    }
    catch (e) {
        return new Response(JSON.stringify({
            error: e.message,
            data: null,
        }), {
            status: 500,
        });
    }
};

const addReactionHandler = (conf, dbQueryKey) => async ({ request }) => {
    const { hash, sig, postId, data } = (await request.json());
    if (!data || !hash || !sig || !postId) {
        return new Response(JSON.stringify({
            error: 'Missing required content',
        }), {
            status: 400,
        });
    }
    /**
     * Emoji validation is now disabled.
     * Reactions can also be used for voting, not only showing emotes.
     * Also, restrictions on displayable emojis as reactions are doing on Reactions.vue.
     */
    // /**
    //  * Check if club has allowListEmojis option
    //  * If not, use default allowListEmojis
    //  */
    // const clubAllowListEmojis =
    // 	(conf.options?.find((item) => item.key === 'allowListEmojis')
    // 		?.value as readonly string[]) ?? emojiAllowList
    // /**
    //  * Only allowlist emojis accepted
    //  */
    // if (!clubAllowListEmojis.includes(emoji)) {
    // 	return new Response(
    // 		JSON.stringify({
    // 			error: 'Emoji not allowed',
    // 		}),
    // 		{
    // 			status: 400,
    // 		},
    // 	)
    // }
    // get user address
    const userAddress = verifyMessage(hash, sig);
    return addReactionDocumentsRedis({
        conf,
        data,
        userAddress,
        postId,
        dbQueryKey,
    });
};

const addPostDocumentsRedis = async ({ conf, data, dbQueryKey, }) => {
    try {
        const client = await getDefaultClient();
        const saved = await setPost({
            scope: dbQueryKey,
            post: data,
            url: conf.url,
            client,
        });
        return saved
            ? new Response(JSON.stringify({
                message: saved,
                data: encode(data),
            }), {
                status: 200,
            })
            : new Response(JSON.stringify({
                error: 'Some data is missing',
                data: null,
            }), {
                status: 400,
            });
    }
    catch (e) {
        return new Response(JSON.stringify({
            error: e.message,
            data: null,
        }), {
            status: 500,
        });
    }
};

const addPostHandler = (conf, dbQueryKey, memberships, roles) => async ({ request }) => {
    const { contents, hash, sig } = (await request.json());
    if (!hash || !sig) {
        return new Response(JSON.stringify({
            error: 'Hash or Signature are missing',
            data: null,
        }), {
            status: 400,
        });
    }
    const skipAuthentication = conf.propertyAddress === ZeroAddress;
    const created_by = verifyMessage(hash, sig);
    const authenticated = !skipAuthentication &&
        ((await whenDefinedAll([hash, sig], ([h, s]) => authenticate({
            message: h,
            signature: s,
            previousConfiguration: encode(conf),
            provider: getDefaultProvider(conf.rpcUrl),
        }))) ||
            (await hasWritePermission({
                account: created_by,
                provider: getDefaultProvider(conf.rpcUrl),
                propertyAddress: conf.propertyAddress,
                memberships,
                roles,
            })));
    // const { randomBytes, recoverAddress, hashMessage } = utils
    const id = uuidFactory(conf.url)();
    const now = new Date();
    const created_at = now;
    const updated_at = now;
    const decodedContents = decode(contents);
    const comments = [];
    const reactions = {};
    const composed = {
        ...decodedContents,
        id,
        created_by,
        created_at,
        updated_at,
        comments,
        reactions,
    };
    if (!authenticated) {
        return new Response(JSON.stringify({ message: 'not allowed' }), {
            status: 401,
        });
    }
    return addPostDocumentsRedis({
        conf,
        data: composed,
        dbQueryKey,
    });
};
/**
 * This can be used to fetch a single Post by ID
 * @param options the all options
 * @param options.dbQueryKey the scope of the query
 * @param options.config the ClubsConfigutation
 * @returns a single post
 */
const fetchPostHandler = ({ dbQueryKey, config, memberships, }) => async ({ request, url, }) => {
    const client = await getDefaultClient();
    /** get the parent post id */
    const pathquery = aperture(2, decodeURIComponent(url.pathname).split('/'));
    const [, postId] = pathquery.find(([key]) => key === 'message') ?? [];
    const { hash, sig } = {
        hash: url.searchParams.get('hash'),
        sig: url.searchParams.get('sig'),
    };
    const reader = whenDefinedAll([hash, sig], ([_hash, _sig]) => {
        return tryCatch(([_h, _s]) => whenDefinedAll([_h, _s], ([h, s]) => verifyMessage(h, s)), (err) => err)([_hash, _sig]);
    });
    try {
        /**
         * fetch the post
         */
        const post = (await whenDefined(postId, (id) => fetchSinglePost({ id, client }))) ??
            new Error('Post ID not found');
        const mask = await whenNotErrorAll([reader, post], ([user]) => maskFactory({
            user,
            propertyAddress: config.propertyAddress,
            rpcUrl: config.rpcUrl,
            memberships: memberships,
        }));
        const result = await whenNotErrorAll([post, mask], ([p, maskFn]) => Promise.all([p].map(maskFn)));
        if (result instanceof Error) {
            return new Response(JSON.stringify({
                error: 'Post not found',
                data: null,
            }), {
                status: 404,
            });
        }
        return new Response(JSON.stringify({
            contents: encode(result),
        }), {
            status: 200,
        });
    }
    catch (e) {
        return new Response(JSON.stringify({
            error: e.message,
            data: null,
        }), {
            status: 500,
        });
    }
};
/**
 * Deletes a single post
 * @param dbQueryKey the scope of the query
 * @returns a single post
 */
const deletePostHandler = (dbQueryKey) => async ({ request }) => {
    const { postId, hash, sig } = (await request.json());
    const client = await getDefaultClient();
    /** get the parent post id */
    // const splitUrl = url.pathname.split('/')
    // const postId = splitUrl[splitUrl.length - 2]
    const createdBy = verifyMessage(hash, sig);
    const success = await deletePost({
        postId,
        client,
        scope: dbQueryKey,
        userAddress: createdBy,
    });
    // eslint-disable-next-line functional/no-expression-statement
    await client.quit();
    return success
        ? new Response(JSON.stringify({ success: true }), {
            status: 200,
        })
        : new Response(JSON.stringify({ success: false }), {
            status: 500,
        });
};

const getAllPosts = async (opts, page) => {
    // eslint-disable-next-line no-case-declarations
    const client = await getDefaultClient();
    // eslint-disable-next-line no-case-declarations
    const result = await getPaginatedPosts({
        scope: opts.key,
        client,
        page: page ?? 0,
    });
    // eslint-disable-next-line functional/no-expression-statement
    await client.quit();
    return result;
};

const headers = { 'Content-Type': 'application/json' };

var SlotName;
(function (SlotName) {
    SlotName["PostsEditAfterContentForm"] = "posts:edit:after:content-form";
    SlotName["PostsEditToolbarButton"] = "posts:edit:toolbar:button";
    SlotName["PostsFeedAfterPostContent"] = "posts:feed:after:post-content";
})(SlotName || (SlotName = {}));
var Event;
(function (Event) {
    Event["PostCreated"] = "posts:event::post_created";
    Event["RegisterOnUpdateHandler"] = "posts:event::register_on_update_handler";
    Event["RegisterOnSetupHandler"] = "posts:event::register_on_setup_handler";
    Event["ClickToolbar"] = "posts:event::click_toolbar";
})(Event || (Event = {}));

const AVAILABLE_VALUE = /^[\w#-]+$/;
const normalize = (q) => q.replaceAll('-', ' ');
/**
 * This can be used to fetch a single Post by ID
 * @param dbQueryKey the scope of the query
 * @param config the ClubsConfiguration
 * @returns a single post
 */
const fetchPostHas = (dbQueryKey, config, memberships) => async ({ url }) => {
    const pathquery = aperture(2, decodeURIComponent(url.pathname).split('/'));
    const [, hasOption] = pathquery.find(([key]) => key === 'has:option') ?? [];
    const limit = 10;
    const { hash, sig, _page } = {
        hash: url.searchParams.get('hash'),
        sig: url.searchParams.get('sig'),
        _page: url.searchParams.get('page'),
    };
    const safeHasOption = whenDefined(hasOption, (q) => AVAILABLE_VALUE.test(q) ? q : new Error('Invalid query')) ?? new Error('Query is required');
    const page = ((n) => (n < 0 ? 0 : n))(parseInt(_page ?? '0'));
    const start = page > 0 ? (page - 1) * limit : 0;
    const reader = whenDefinedAll([hash, sig], ([h, s]) => tryCatch(([v1, v2]) => verifyMessage(v1, v2), (err) => err)([h, s]));
    const query = whenNotError(safeHasOption, (q) => [
        `@${_scope['$._scope'].AS}:{${uuidToQuery(dbQueryKey)}}`,
        `@${_parent_type['$._parent_type'].AS}:post`,
        `@${key['$.key'].AS}:${normalize(q)}`,
    ].join(' '));
    const client = await getDefaultClient();
    const optionsResult = (await whenNotError(query, (q) => client.ft
        .search(Index.Option, q, {
        LIMIT: {
            from: start,
            size: limit,
        },
    })
        .catch((err) => err))) ?? new Error('Qeury is required');
    const posts = await whenNotError(optionsResult, (opts) => Promise.all(opts.documents
        .map((opt) => opt.value)
        .filter((opt) => opt !== null)
        .map((opt) => fetchSinglePost({ id: opt._parent_id, client }))));
    const mask = await whenNotError(reader, (user) => maskFactory({
        user,
        propertyAddress: config.propertyAddress,
        rpcUrl: config.rpcUrl,
        memberships,
    }));
    const results = await whenNotErrorAll([posts, mask], ([data, maskFn]) => Promise.all(data.map(maskFn)));
    return results instanceof Error
        ? new Response(JSON.stringify({
            error: results.message,
        }), {
            status: 500,
            headers,
        })
        : new Response(JSON.stringify({
            contents: encode(results),
        }), {
            status: 200,
            headers,
        });
};

const getPagePaths = (async (options, { propertyAddress, adminRolePoints, rpcUrl }, { getPluginConfigById }) => {
    const [membershipsPlugin] = getPluginConfigById('devprotocol:clubs:simple-memberships');
    const memberships = membershipsPlugin?.options?.find(({ key }) => key === 'memberships')?.value;
    const dbs = options.find(({ key }) => key === 'feeds')?.value;
    const emojiAllowList = options?.find((item) => item.key === 'emojiAllowList')
        ?.value;
    const props = {
        options,
        propertyAddress,
        memberships,
        adminRolePoints,
        emojiAllowList,
        rpcUrl,
    };
    return dbs
        ? [
            ...dbs.map(({ id, slug }) => {
                return {
                    paths: [slug ?? 'posts'],
                    component: Posts_,
                    props: { ...props, feedId: id },
                    // `propertyAddress` is required for calling post API, so passed to the FE here.
                };
            }),
            ...dbs.map(({ id }) => {
                return {
                    paths: ['posts', id, SinglePath],
                    component: Posts_,
                    props: { ...props, feedId: id },
                };
            }),
        ]
        : [];
});
const getAdminPaths = (async (options, config, { getPluginConfigById }) => {
    const [membershipsPlugin] = getPluginConfigById('devprotocol:clubs:simple-memberships');
    const memberships = membershipsPlugin?.options?.find(({ key }) => key === 'memberships')?.value;
    const feeds = options.find(({ key }) => key === 'feeds')
        ?.value ?? [];
    return [
        {
            paths: ['posts'],
            component: ListFeed,
            props: { options },
        },
        {
            paths: ['posts', 'new'],
            component: Feed,
            props: { feeds, url: config.url, memberships },
        },
        ...feeds.map((feed) => ({
            paths: ['posts', 'edit', feed.id],
            component: Feed,
            props: { feeds, url: config.url, edit: feed, memberships },
        })),
    ];
});
const meta = {
    id: 'devprotocol:clubs:plugin:posts',
    displayName: 'Posts',
    description: 'Add an interactive posts feed to your Club',
    category: ClubsPluginCategory.Uncategorized,
    previewImages: [Screenshot1.src, Screenshot2.src, Screenshot3.src],
    readme: Readme,
    icon: Icon.src,
};
const getApiPaths = (async (options, config, { getPluginConfigById }) => {
    v5(config.url, v5.URL);
    encode(config);
    const dbs = options.find(({ key }) => key === 'feeds')?.value;
    const [membershipsPlugin] = getPluginConfigById('devprotocol:clubs:simple-memberships');
    const memberships = membershipsPlugin?.options?.find(({ key }) => key === 'memberships')?.value;
    if (!dbs) {
        return [];
    }
    return [
        {
            paths: ['indexing', 'documents:redis'],
            method: 'POST',
            handler: async () => {
                const client = await getDefaultClient();
                const res = await createIndex();
                // eslint-disable-next-line functional/no-expression-statement
                await client.quit();
                return new Response(JSON.stringify(res));
            },
        },
        {
            paths: ['options', 'feeds'],
            method: 'GET',
            handler: async () => {
                return new Response(JSON.stringify(dbs), {
                    headers,
                });
            },
        },
        ...dbs
            .map((db) => [
            {
                paths: [db.id, 'profile'],
                method: 'GET',
                handler: async ({ url }) => {
                    const address = url.searchParams.get('address');
                    if (!address) {
                        return new Response(JSON.stringify({
                            error: 'Address is missing',
                            data: null,
                        }), {
                            status: 400,
                        });
                    }
                    //
                    try {
                        const res = await fetchProfile(address);
                        if (res.error) {
                            return new Response(JSON.stringify({
                                error: res.error,
                                data: null,
                            }), {
                                status: 500,
                            });
                        }
                        return new Response(JSON.stringify({
                            profile: res.profile,
                        }), {
                            status: 200,
                        });
                    }
                    catch (e) {
                        return new Response(JSON.stringify({
                            error: e,
                            data: null,
                        }), {
                            status: 500,
                        });
                    }
                },
            },
            /**
             * delete post
             */
            {
                paths: [db.id, 'message', 'delete'],
                method: 'POST',
                handler: deletePostHandler(db.database.key),
            },
            {
                paths: [db.id, 'message'],
                // This will be [POST] /api/devprotocol:clubs:plugin:posts/{FEED_ID}/message
                method: 'POST',
                handler: addPostHandler(config, db.database.key, memberships ?? [], db.roles),
            },
            /**
             * For fetching paginated posts
             */
            {
                paths: [db.id, 'message'],
                method: 'GET',
                handler: async ({ url }) => {
                    const { hash, sig, page } = {
                        hash: url.searchParams.get('hash'),
                        sig: url.searchParams.get('sig'),
                        page: url.searchParams.get('page'),
                    };
                    const parsedPage = parseInt(page ?? '0');
                    // eslint-disable-next-line functional/no-let
                    let reader;
                    try {
                        // eslint-disable-next-line functional/no-expression-statement
                        reader = whenDefinedAll([hash, sig], ([h, s]) => verifyMessage(h, s));
                    }
                    catch (error) {
                        // eslint-disable-next-line functional/no-expression-statement
                        console.log(error);
                    }
                    // eslint-disable-next-line functional/no-let
                    let allPosts;
                    // eslint-disable-next-line
                    try {
                        const _allPosts = await whenDefinedAll([db.database.key], ([key]) => getAllPosts({ key }, parsedPage));
                        const mask = await maskFactory({
                            user: reader,
                            propertyAddress: config.propertyAddress,
                            rpcUrl: config.rpcUrl,
                            memberships: memberships ?? [],
                        });
                        // eslint-disable-next-line
                        allPosts =
                            (await whenDefinedAll([mask, _allPosts], ([maskFn, posts]) => posts instanceof Error
                                ? posts
                                : Promise.all(posts.map(maskFn)))) ?? _allPosts;
                    }
                    catch (error) {
                        return new Response(JSON.stringify({
                            error: error,
                        }), {
                            status: 500,
                        });
                    }
                    return allPosts instanceof Error
                        ? new Response(JSON.stringify({
                            error: allPosts,
                        }), {
                            status: 500,
                        })
                        : allPosts
                            ? new Response(JSON.stringify({
                                contents: encode(allPosts),
                            }), {
                                status: 200,
                            })
                            : new Response(JSON.stringify({
                                error: 'Some data is missing',
                            }), {
                                status: 400,
                            });
                },
            },
            /**
             * Fetch individual post
             */
            {
                paths: [db.id, 'message', /((?!\/).)+/],
                method: 'GET',
                handler: fetchPostHandler({
                    dbQueryKey: db.database.key,
                    config,
                    memberships: memberships ?? [],
                }),
            },
            /**
             * Fetch paginated comments by post id
             */
            {
                paths: [db.id, 'message', /((?!\/).)+/, 'comments'],
                method: 'GET',
                handler: fetchCommentsHandler(config),
            },
            {
                paths: [db.id, 'comment'], // This will be [POST] /api/devprotocol:clubs:plugin:posts/{FEED_ID}/comment
                method: 'POST',
                handler: addCommentHandler(config, db.database.key),
            },
            {
                paths: [db.id, 'comment', 'delete'], // This will be [POST] /api/devprotocol:clubs:plugin:posts/{FEED_ID}/comment
                method: 'POST',
                handler: deleteCommentHandler(config, db.database.key),
            },
            {
                paths: [db.id, 'reactions'], // This will be [POST] /api/devprotocol:clubs:plugin:posts/{FEED_ID}/reactions
                method: 'POST',
                handler: addReactionHandler(config, db.database.key),
            },
        ])
            .flat(),
        ...dbs.map((db) => ({
            paths: [db.id, 'search', /.*/],
            method: 'GET',
            handler: fetchPostHas(db.database.key, config, memberships ?? []),
        })),
    ];
});
const getSlots = (async (options, __, { paths, factory }) => {
    const [path1, path2, path3] = paths;
    const feeds = options.find(({ key }) => key === 'feeds')
        ?.value ?? [];
    const newsFeeds = feeds.filter((feed) => feed.slots?.[ClubsSlotName.PageContentHomeBeforeContent] !== undefined &&
        feed.slots[ClubsSlotName.PageContentHomeBeforeContent].enabled);
    const newsSlots = newsFeeds.length > 0 && factory === 'page'
        ? newsFeeds
            .map((feed) => {
            return [
                {
                    slot: ClubsSlotName.PageContentHomeBeforeContent,
                    component: News,
                    props: {
                        scope: feed.database.key,
                        feedId: feed.id,
                        slots: feed.slots,
                    },
                },
            ];
        })
            .flat()
        : [];
    return [
        ...(factory === 'page' // == The public feed page with PageContentHomeBeforeContent slot
            ? newsSlots
            : []),
        ...(factory === 'admin' && path1 === 'posts' && path2 === undefined // == The feeds list page
            ? [
                {
                    slot: 'admin:aside:after-built-in-buttons',
                    component: NavigationLink,
                    props: {
                        navigation: {
                            display: 'Create a new feed',
                            path: '/admin/posts/new',
                        },
                    },
                },
            ]
            : []),
        ...((feed) => feed &&
            factory === 'admin' &&
            path1 === 'posts' &&
            path2 === 'edit' &&
            path3 // == The feed edit page
            ? [
                {
                    slot: 'admin:aside:after-built-in-buttons',
                    component: CreateNavigationLink,
                    props: {
                        createNavigation: {
                            label: `Add '${feed.title ?? 'Posts'}' to the menu`,
                            link: {
                                display: feed.title ?? 'Posts',
                                path: `/${feed.slug ?? 'posts'}`,
                            },
                        },
                    },
                },
            ]
            : [])(feeds.find((feed) => feed.id === path3)),
    ];
});
var index = {
    getPagePaths,
    getAdminPaths,
    getApiPaths,
    getSlots,
    meta,
};

export { Event, SlotName, index as default, getAdminPaths, getApiPaths, getPagePaths, getSlots, meta };
