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 } 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 mapTiles({ pg = pgClients.client, params = {}, query = {}, unittest }, reply) {
    const { slug, y, z } = params;
    const { sql, nocache, filter, pointZoom, type, layers, layer } = query;
    const x = params.x?.split?.('.')?.[0] - 0;

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

    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, layer].filter(Boolean).join();

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

    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);
        }
    }

    const loadTemplate = await getTemplate('map', slug);

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

    const index = loadTemplate?.find?.(el => el[0].split('.').shift() === 'index')?.[1];

    const layers1 = !index
        ? loadTemplate?.layers
        : loadTemplate?.filter?.(el => el[0].split('.').shift() !== 'index')?.map(el => ({ ...el[1], id: el[0].replace('.yml', '').replace('.json', '') }));

    if (!layers1?.length) {
        return reply.status(404).send('map layers not found');
    }

    layers1.forEach(el => Object.assign(el, { name: el.name || el.id }));

    const layers2 = layers1.filter(el => el.name && el.source_path).filter(el => layers ? layers.includes(el.name) : true).filter(el => layer ? el.name === layer : true);

    if (!layers2.length) {
        return reply.status(404).send('map layers sources not provided / invalid query params');
    }

    const allFilters = loadTemplate.widgets?.filter?.(el => el.type === 'filters') || [];
    const { filters: filterList = [], layer_id: layerId } = allFilters.find(el => layer ? el.config?.layer_id === layer : el.default)?.config || allFilters[0]?.config || {};

    const q = (await Promise.all(layers2.map(async (layer) => {
        const { source_path: source, id, name, style, geometry_type: gt, query: lquery, geometry_column: gCol } = layer;
        const loadTable = await getTemplate('table', source);

        const table = loadTable?.table || source;
        const meta = await getMeta({ pg, table });
        const { columns = [] } = meta || {};
        const geom = gCol || meta?.geom || 'geom';
        if (!columns?.length) return null;

        const pk = pg.pk?.[table] || loadTable?.key;
        const fData = filter && layerId === name ? await getFilterSQL({ pg, table: source, filterList, 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 => {
            return el.type?.[0] === '_'
                ? `${el.name}[1] as ${el.name}`
                : `${el.name}${['int4'].includes(el.type) ? '::text' : ''}`;
        });

        return `SELECT ST_AsMVT(q, '${(id || name).replace(/'/g, "''")}', 4096, 'geom','row') as tile
                      FROM (
                        SELECT  /*substr(${pk},12)::int*/ /*${(y - 0 + x) * 10000} + row_number() over()*/ 

                           floor(random() * 100000 + 1)::int + row_number() over() as row,
                            
                           ${style?.colorAttr ? `${style.colorAttr},` : ''} ${props?.length ? `${props.map(el => `"${el}"`).join(',')},` : ''}

                            ${pk} as  id, 
                            ST_AsMVTGeom(st_transform(${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 ${lquery || '1=1'}
                        
                        ${gt === 'polygon' ? `order by st_area(st_transform(${geom},3857)) desc` : ''}

                        limit ${limit})q
                      ) q`;
    }))).filter(el => el).join(' union all ');

    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(slug, 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);
}