import path from 'node:path';
import { createHash } from 'node:crypto';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';

import {
    getTemplatePath, addHook, getToken, getTemplate, config, pgClients, initPG, getRedis, logger, getMenu,
} from '@opengis/fastify-table/utils.js';

const { client } = pgClients;

const rclient = getRedis();
const cwd = process.cwd();

const logDir = path.join(cwd, 'log/migration');

export default async function plugin(fastify) {
    const user1 = config?.auth?.disable || process.env.NODE_ENV !== 'admin' ? { uid: '1' } : null;
    await initPG(client);

    addHook('preForm', async ({ form, user }) => {
        if (!user?.uid) return null;
        const opt = await getToken({ mode: 'w', token: form, uid: user.uid, json: 1 });
        return opt;
    });

    fastify.addHook('onListen', async () => {
        const json = await getMenu({ user: { uid: 1 } });
        // insert interface list to db (user access management)
        if (client?.pk?.['admin.routes'] && json?.menus?.length) {
            const menuList = json?.menus?.filter?.((el) => (el?.table || el?.component || el?.menu?.length) /*&& el?.ua || el?.en || el?.name*/) || [];

            // skip dupes
            //admin_route_menu_id_fkey
            menuList.forEach(el => Object.assign(el, { ua: el?.ua || el?.en || el?.name }));
            const uniqueList = menuList.filter((el, idx, arr) => el?.ua && arr.map((item) => item?.ua).indexOf(el?.ua) === idx);
            const q = `insert into admin.menu(name, ord) values${uniqueList.map((el, i) => `('${el?.ua.replace(/'/g, "''")}', ${i}) `).join(',')}
            on conflict (name) do update set ord=excluded.ord, enabled=true returning name, menu_id`;

            const { rows = [] } = uniqueList.length ? await client.query(q) : {};
            const menus = rows.reduce((acc, curr) => Object.assign(acc, { [curr.menu_id]: uniqueList.find((item) => item?.ua === curr.name) }), {});

            const values = Object.entries(menus).reduce((acc, curr) => {
                if (curr[1]?.table || curr[1]?.component) { acc.push({ ...curr[1], menuId: curr[0] }); }
                curr[1]?.menu?.forEach((el) => acc.push({ ...el, menuId: curr[0] }));
                return acc;
            }, []);

            await Promise.all(values.filter((el) => el?.table).map(async (el) => {
                const loadTable = await getTemplate('table', el.table);
                Object.assign(el, {
                    table1: loadTable?.table || el.table,
                    actions: loadTable?.actions,
                    access: loadTable?.access,
                });
            }));
            // console.log(values)
            const q1 = `insert into admin.routes(route_id, alias, title, menu_id, table_name, actions, access, query) 
            values ${values.filter((el) => el?.path).map((el) => `('${el.path}', ${el.table ? `'${el.table}'` : null}, 
            ${(el.title || el.ua) ? `'${(el.title || el.ua).replace(/'/g, "''")}'` : null}, 
            ${el.menuId ? `'${el.menuId}'` : null}, ${el.table1 ? `'${el.table1}'` : null}, 
            ${el.actions?.length ? `'{ ${el.actions} }'::text[]` : null}, ${el.access ? `'${el.access}'` : null}, 
            ${el.query ? `'${el.query.replace(/'/g, "''")}'` : '\'1=1\''})`).join(',')} 
            on conflict (route_id) do update set menu_id=excluded.menu_id, alias=excluded.alias, title=excluded.title, enabled=true, query=excluded.query,
            table_name=excluded.table_name, actions=excluded.actions, access=excluded.access returning route_id, table_name`;

            try {
                console.log('admin/hook routes sql start');

                const { rowCount: menuCount } = await client.query(`delete from admin.menu 
                where not array[menu_id] <@ $1::text[] and menu_id not in (select menu_id from admin.routes)`, [rows.map((el) => el.menu_id)]);
                console.log('delete deprecated menus ok', menuCount);

                const { rowCount: interfaceCount } = await client.query(`delete from admin.routes 
                where not array[route_id] <@ $1::text[] and route_id not in (select route_id from admin.role_access)`, [values.filter((el) => el?.path)]);
                console.log('delete deprecated interfaces ok', interfaceCount);

                const { rowCount } = values?.length ? await client.query(q1) : {};
                console.log('insert interfaces ok', rowCount);
            } catch (err) {
                console.log('admin/hook routes sql error', values, q1, err);
            }
        }
    });

    fastify.addHook('onListen', async () => {
        const clsQuery = [];
        if (!client?.pk?.['admin.cls']) return;

        const selectList = getTemplatePath('select');
        const clsList = (getTemplatePath('cls'))?.filter((el) => !(selectList?.map((el) => el?.[0]) || []).includes(el[0]));
        const cls = (selectList || []).concat(clsList || [])
            ?.map((el) => ({ name: el[0], module: path.basename(path.dirname(path.dirname(el[1]))), type: { 'json': 'cls', 'sql': 'select' }[el[2]] }))
        if (!cls?.length) return;

        try {
            const hashes = await rclient.hgetall('cls-insert-hashes').then(obj => Object.keys(obj));
            const dbdata = await client.query(`select json_object_agg(name, hash) from admin.cls where parent is null`).then(el => el.rows?.[0]?.json_object_agg || {});
            const names = Object.keys(dbdata);
            console.log('admin/hook cls promise start');
            const qHashes = await Promise.all(cls.filter((el, idx, arr) => arr.map((item) => item.name).indexOf(el.name) === idx).map(async (el) => {
                const { name, module, type } = el;
                const loadTemplate = await getTemplate(type, name);
                el.hash = createHash('md5').update(type === 'cls' ? JSON.stringify(loadTemplate) : (loadTemplate?.sql || loadTemplate)).digest('hex');
                el.dbhash = dbdata[name];

                // check for changes by redis hash / dropped from db / changed at git project
                el.update = !hashes.includes(el.hash) || !names.includes(name) || el.hash !== el.dbhash;

                if (type === 'select' && (loadTemplate?.sql || loadTemplate) && el.update) {
                    clsQuery.push(`insert into admin.cls(name,type,data,module,hash) values('${name}','sql','${(loadTemplate?.sql || loadTemplate)?.replace(/'/g, "''")}', '${module?.replace(/'/g, "''")}','${el.hash}')`);
                    if (config.trace) console.log(name, type, 'insert fresh select');
                    return el.hash;
                } else if (type === 'cls' && loadTemplate?.length && (el.update || Object.hasOwn(loadTemplate?.[0] || {}, 'color'))) {
                    clsQuery.push(`insert into admin.cls(name,type, module,hash) values('${name}','json', '${module?.replace(/'/g, "''")}','${el.hash}'); 
                insert into admin.cls(code,name,parent,icon,color,data) 
                select value->>'id',value->>'text','${name}',value->>'icon',value->>'color',value->>'data' 
                from json_array_elements('${JSON.stringify(loadTemplate).replace(/'/g, "''")}'::json) on conflict (code,parent) do update set color=excluded.color;`);
                    if (config.trace) console.log(name, type, 'insert fresh cls');
                    return el.hash;
                } else if (hashes.includes(el.hash)) {
                    if (config.trace) console.log(name, type, names.includes(name) ? 'skip equal hash' : 'insert missing cls');
                    return el.hash;
                } else {
                    if (config.trace) console.log(name, type, 'empty');
                    return el.hash;
                }
            }));

            // debug
            mkdirSync(logDir, { recursive: true });
            writeFileSync(path.join(logDir, `${path.basename(cwd)}-${client.options?.database}-cls.sql`), clsQuery.filter((el) => el).join(';'));
            writeFileSync(path.join(logDir, `${path.basename(cwd)}-${client.options?.database}-cls.json`), JSON.stringify(cls));

            const { rowCount = 0 } = await client.query('delete from admin.cls where $1::text[] && array[name,parent]', [cls.filter(el => el.update).map(el => el.name)]);
            console.log('admin/hook old cls deleted', rowCount);
            if (clsQuery.filter((el) => el).length) {
                console.log('admin/hook cls sql start', clsQuery?.length);
                await client.query(clsQuery.filter((el) => el).join(';'));
                await Promise.all(qHashes.filter(el => el).map(async (el) => rclient.hset('cls-insert-hashes', el, 1)));
                logger.file('migration/hash', { list: qHashes.filter(el => el) });
                console.log('admin/hook cls sql finish', clsQuery?.length);
            }
            console.log('admin/hook cls promise finish', rowCount);
        } catch (err) {
            console.error('admin/hook cls sql error', err.toString());
            console.trace(err);
        }
    });

    addHook('afterTable', async ({ table, res = {}, payload: rows = [], user = {} }) => {
        const loadTable = await getTemplate('table', table);
        const { uid } = user1 || user;
        if (!uid || !table || !client?.pk?.[table] || !rows.length || !loadTable?.table) return;

        // admin.custom_column - user column data
        const { rows: properties = [] } = await client.query(`select column_id, name, title, format, data from admin.custom_column 
        where _table and entity=$1 and uid=$2`, [table, uid]);
        const extraColumnList = properties.map((row) => ({ id: row.column_id, name: row.name, title: row.title, format: row.format, data: row.data }));
        if (!extraColumnList?.length) return;

        if (res?.columns?.length) {
            extraColumnList.forEach((col) => res.columns.push(col));
        }

        const { rows: extraData = [] } = await client.query(`select object_id, json_object_agg( property_id, coalesce(value_date::text,value_text) ) as extra from crm.extra_data 
        where property_entity=$1 and property_id=any($2) and object_id=any($3) group by object_id`, [table, extraColumnList?.map((el) => el.id), rows.map((el) => el.id)]);

        if (!extraData?.length) {
            // Object.assign(rows?.[0] || {}, { ...extraColumnList.reduce((acc, curr) => Object.assign(acc, { [curr.name]: null }), {}) });
            return;
        }

        rows.filter((row) => extraData.map((el) => el?.object_id).includes(row.id)).forEach((row) => {
            const { extra = {} } = extraData.find((el) => el.object_id === row.id);
            Object.assign(row, { ...Object.fromEntries(Object.entries(extra).map((el) => [extraColumnList.find((col) => col.id === el[0]).name, el[1]])) });
        });

        // admin.custom_column - metaFormat
        await Promise.all(extraColumnList.filter((el) => el?.data).map(async (attr) => {
            const values = [...new Set(rows?.map((el) => el[attr.name]).flat())].filter((el) => el);
            if (!values.length) return;
            const cls = await getSelectVal({ name: attr.data, values });
            if (!cls) return;
            rows.forEach(el => {
                const val = el[attr.name]?.map?.(c => cls[c] || c) || cls[el[attr.name]] || el[attr.name];
                if (!val) return;
                Object.assign(el, { [val?.color ? `${attr.name}_data` : `${attr.name}_text`]: (val.color ? val : val.text || val) });
            });
        }));
    });

    // extract table from form token for user columns - p.2 - read (refactor to global token)
    addHook('preTemplate', async ({ name, type, user = {} }) => {
        if (!name || !type) return;
        const { uid } = user1 || user;
        const tokenData = await getToken({
            uid, token: name, mode: 'w', json: 1,
        })  // edit?
            || await getToken({
                uid, token: name, mode: 'a', json: 1,
            }) || {}; // add?
        return { name: tokenData?.[type] };
    });

    addHook('afterTemplate', async ({ name, type, payload: data = {}, user = {} }) => {
        const { uid } = user1 || user;
        // extract table from form token for user columns - p.1 - assign (refactor to global token)
        if (!uid || !data || type !== 'form' || !name) return null;

        const { form, id, table } = await getToken({
            uid, token: name, mode: 'w', json: 1,
        })  // edit?
            || await getToken({
                uid, token: name, mode: 'a', json: 1,
            }) || {}; // add?

        const { rows: properties = [] } = await client.query(`select name, title, format, data from admin.custom_column 
        where entity=$1 and uid=$2`, [table || name, uid]);

        await Promise.all(properties.map(async (el) => {
            const clsData = el.data ? await getTemplate(['cls', 'select'], el.data) : undefined;
            const type = clsData ? 'Select' : ({ date: 'DatePicker' }[el.format] || 'Text');
            Object.assign(data?.schema || data || {}, { [el.name]: { type, ua: el.title, data: el.data, options: type === 'Select' && Array.isArray(clsData) && clsData?.length ? clsData : undefined, extra: 1 } });
        }));
    });

    addHook('afterUpdate', async ({ table, body = {}, payload: res = {}, user = {} }) => {
        const { uid } = user1 || user;
        if (!uid || !table || !Object.keys(body)?.length) return null;

        const loadTable = await getTemplate('table', table);
        if (!client?.pk?.[loadTable?.table || table]) return null;
        const pk = client?.pk?.[loadTable?.table || table];
        const id = res[pk];

        const { rows: properties = [] } = await client.query(`select column_id, name, title, format, data from admin.custom_column 
        where entity=$1 and uid=$2`, [table, uid]);

        if (!id || !properties?.length) return null;

        const q = `delete from crm.extra_data where property_entity='${table}' and object_id='${id}';${properties
            .filter((el) => Object.keys(body).includes(el.name))
            .map((el) => `insert into crm.extra_data(property_id,property_key,property_entity,object_id,${el.format?.toLowerCase() === 'date' ? 'value_date' : 'value_text'})
        select '${el.column_id}', '${el.name}', '${table}', '${id}', ${el.format?.toLowerCase() === 'date' ? `'${body[el.name]}'::timestamp without time zone` : `'${body[el.name]}'::text`}`)
            .join(';\n') || ''}`;
        return client.query(q);
    });

    addHook('afterInsert', async ({ table, body = {}, payload: res = {}, user = {} }) => {
        const { uid } = user1 || user;
        if (!uid || !table || !Object.keys(body)?.length) return null;

        const loadTable = await getTemplate('table', table);
        if (!client?.pk?.[loadTable?.table || table]) return null;
        const pk = client?.pk?.[loadTable?.table || table];
        const id = res.rows?.[0]?.[pk];

        const { rows: properties = [] } = await client.query(`select column_id, name, title, format, data from admin.custom_column 
        where entity=$1 and uid=$2`, [table, uid]);

        if (!id || !properties?.length) return null;

        const q = properties
            .filter((el) => Object.keys(body).includes(el.name))
            .map((el) => `insert into crm.extra_data(property_id,property_key,property_entity,object_id,${el.format?.toLowerCase() === 'date' ? 'value_date' : 'value_text'})
        select '${el.column_id}', '${el.name}', '${table}', '${id}', ${el.format?.toLowerCase() === 'date' ? `'${body[el.name]}'::timestamp without time zone` : `'${body[el.name]}'::text`}`)
            .join(';\n');
        return client.query(q);
    });
}