import path from 'node:path';
import { existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import {
    readFile, stat, mkdir, writeFile,
} from 'node:fs/promises';

import Sphericalmercator from '@mapbox/sphericalmercator';

import {
    getTemplate, pgClients, getFilterSQL, getMeta, yml2json, getColumnCLS, getSelect,
} from '@opengis/fastify-table/utils.js';

import rootFolder from '../../../plugins/mapnik/funcs/rootFolder.mjs';

const limit = 3000;
const mercator = new Sphericalmercator({ size: 256 });
const types = { point: 'ST_Point', polygon: 'ST_Polygon,ST_MultiPolygon' };
const systemColumns = ['uid', 'cdate', 'editor_id', 'editor_date', 'created_by', 'created_at', 'updated_by', 'updated_at'];

export default async function vtile({
    pg = pgClients.client, params = {}, query = {}, unittest,
}, reply) {
    const { id, y, z } = params;
    const {
        sql, nocache, filter, pointZoom, type, layers,
    } = query;
    const x = params.x?.split?.('.')?.[0] - 0;

    if (!id) {
        return reply.status(400).send('not enough params: id');
    }

    if (!x || !y || !z) {
        return reply.status(400).send('not enough params: xyz');
    }

    const bbox = mercator.bbox(+y, +x, +z, false);
    const bbox2d = `'BOX(${bbox[0]} ${bbox[1]},${bbox[2]} ${bbox[3]})'::box2d`;

    const headers = {
        'Content-Type': 'application/x-protobuf',
        'Cache-Control': nocache || sql ? 'no-cache' : 'public, max-age=86400',
    };

    const hash = [filter, layers].filter((el) => el).join();

    const filepath = path.join(
        rootFolder,
        `/vtile/${id}/${hash ? `${createHash('md5').update(hash).digest('hex')}/` : ''}${z}/${x}/${y}.vmt`,
    );

    if (existsSync(filepath) && z < 13 && !nocache && !sql && !unittest) {
        const { birthtimeMs = Date.now() } = await stat(filepath);
        const ageInMs = Date.now() - birthtimeMs;
        if (ageInMs < 86400000) {
            const buffer = await readFile(filepath);
            return reply.status(200).headers(headers).send(buffer);
        }
    }

    if (!pg.pk?.['gis.services']) {
        return reply.status(404).send('services table not found');
    }

    const data = await pg.query(
        'select source_path as source, style, geom_type as gt, geometry_column as gc from gis.services where service_id=$1 and is_active and is_public',
        [id],
    ).then(el => el.rows?.[0]);

    if (!data) {
        return reply.status(404).send('service not found');
    }

    const { source, gt, gc } = data;

    const style = data.style ? yml2json(data.style) : {};

    if (!source) {
        return reply.status(400).send('invalid service source path');
    }

    const loadTable = await getTemplate('table', source);

    const table = loadTable?.table || source;

    if (!pg.pk?.[table]?.length) {
        return reply.status(404).send('layer table not found');
    }

    const cls = getColumnCLS(table);

    const meta = await getMeta({ pg, table });
    const { columns = [] } = meta || {};

    const geom = gc || meta?.geom || 'geom';
    const pk = pg.pk?.[table] || loadTable?.key;
    const fData = filter ? await getFilterSQL({ pg, table: source, filter }) : {};

    const geomCol = parseInt(z, 10) < parseInt(pointZoom || '0', 10) || (style?.centroid && gt === 'point')
        ? `ST_Centroid(${geom})`
        : geom;

    const columnsFiltered = columns
        .filter(el => !['json', 'geometry'].includes(pg.pgType?.[el.dataTypeID]) && !systemColumns.includes(el.name));

    const props = columnsFiltered.map(el => (el.type?.[0] === '_'
        ? `${el.name}[1] as ${el.name}`
        : `${el.name}${['int4'].includes(el.type) ? '::text' : ''}`));

    const propsColumns = props.length ? await Promise.all(props.map(async colname => {
        if (!cls?.[colname]) return `"${colname}"`;
        const { type: type2, data: data2 } = await pg.queryCache('select type, data from admin.cls where parent is null and name=$1 limit 1', { args: [cls[colname]], table: 'admin.cls' })
            .then(el => el.rows?.[0] || {}) || {};

        if (!type2 || (type2 === 'sql' && !data2)) {
            return `"${colname}"`;
        }

        if (type2 === 'json') {
            return `(select name from admin.cls where parent='${cls[colname]}' and code="${colname}"::text) as "${colname}"`;
        }

        return `(with c(id,text) as (select * from (${data2})q limit 1) select text from c where id::text="${colname}"::text) as "${colname}"`;
    })) : '';

    const q = `SELECT ST_AsMVT(q, '${id.replace(/'/g, "''")}', 4096, 'geom','row') as tile
                      FROM (
                        SELECT

                           floor(random() * 100000 + 1)::int + row_number() over() as row,
                            
                           ${style?.colorAttr ? `${style.colorAttr},` : ''} ${propsColumns ? `${propsColumns.filter(Boolean).join('\n,') || ''},` : ''}

                            ${pk} as  id, 
                            ST_AsMVTGeom(st_transform(${style?.type === 'point' ? `st_pointonsurface(${geomCol})` : geomCol},3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,true) geom

                            FROM (select * from ${table} where ${loadTable?.query || '1=1'} and ${fData?.q || 'true'} and ${geom} && ${bbox2d} 
                            
                            and ${geom} is not null and st_srid(${geom}) >0
                            
                        ${types[type] ? ` and ST_GeometryType(${geom}) = any ('{ ${types[type]} }') ` : ''}

                        and 1=1
                        
                        ${gt === 'polygon' ? `order by st_area(st_transform(${geom},3857)) desc` : ''}

                        limit ${limit})q
                      ) q`;

    if (sql === '1') return q;
    if (sql === '2') return filepath.replace(/\\/g, '/');

    const tile = await pg.query(q);

    if (unittest && !tile.rows?.length) {
        console.log(id, q);
    }

    if (sql === '3') return tile.rows.map((el) => el.tile);

    if (sql === '4') return tile.rows.map((el) => el.tile);

    const buffer = Buffer.concat(tile.rows.map((el) => Buffer.from(el.tile)));

    if (!nocache) {
        await mkdir(path.dirname(filepath), { recursive: true });
        await writeFile(filepath, buffer, 'binary');
    }

    return reply.status(200).headers(headers).send(buffer);
}
