const headers = {
  'Content-Type': 'application/x-protobuf',
  'Cache-Control': '86400',
};

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';

const mercator = new Sphericalmercator({ size: 256 });

// import { SphericalMercator } from '@mapbox/sphericalmercator';
// const mercator = new SphericalMercator({ size: 256 });

import {
  getColumnCLS, getTemplate, getMeta, getFilterSQL, getSelect,
} from '@opengis/fastify-table/utils.js';
import yaml from 'js-yaml';
import rootFolder from '../../plugins/mapnik/funcs/rootFolder.mjs';

const propsCache = {};

export default async function vtile({
  params = {}, query, pg, user,
}, reply) {
  const { y, z } = params;
  const x = params.x.split('.')[0] - 0;
  const {
    type, nocache, sql, clusterZoom, filter,
  } = query;

  const hash = [type, clusterZoom, filter].join();

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

  if (existsSync(filepath) && +z < 13 && !nocache && !sql && process.env.NODE_ENV !== 'test') {
    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);
    }
  }

  // layer
  const [map, layer] = params.layer.includes(':') ? params.layer.split(':') : [null, params.layer];

  // service
  const data = await getTemplate('layer', layer) || await pg.query(`select * from gis.services where ${!user?.uid ? 'is_public and is_active' : '1=1'} and service_id=$1`, [layer]).then(el => el.rows?.[0]);

  const geom = data?.geometry_column || 'geom';
  const table = data?.source_path;
  const style = typeof data?.style === 'object' ? data?.style : yaml.load(data?.style);
  const filterList = data?.filters;
  const layerQuery = data?.query;

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

  const filterData = query.filter ? await getFilterSQL({
    filter: query.filter, table, filterList,
  }) : {};

  // meta
  const metaTable = await getMeta({ pg, table });
  if (metaTable.error) { return metaTable; }
  const { pk: pkey = data.key, columns } = metaTable; // if view - specify via template

  // props

  const props = !query.all ? (data.popup || []).concat(data.card || []) : columns.filter(el => !['json', 'geometry'].includes(pg.pgType[el.dataTypeID]) && !['uid', 'cdate', 'editor_id', 'editor_date'].includes(el.name));

  const cache = layer + (query.all ? '-all' : '') + JSON.stringify(data.popup || []).length;
  const propsWithCls = propsCache[cache] ?? await Promise.all(props.map(async (col, idx) => {
    const key = col.name;
    const select = data.popup?.find(el => el.name === key);
    // console.log(select);
    const clsData = select?.data || col.data;
    const cls = clsData ? await getSelect(clsData).then(el => {
      if (!el) {
        console.error('gis vtile: cls not found', clsData);
        return null;
      }
      return { name: clsData, type: el.arr ? 'cls' : 'select', sql: el.sql };
    }) : null;
    const { name, type: type1, sql: sql1 } = cls || getColumnCLS(table, col.data || key) || {};
    // console.log({ name, type, sql });
    if (name && type1 === 'cls') {
      return `(select name from admin.cls where parent='${name}' and code::text=q.${key}::text limit 1) as "${key}_text","${key}"::text`;
    }
    if (type1 === 'select' && sql1) {
      const { fields = [] } = await pg.query(`${sql1} limit 0`);
      return `(select "${fields[1].name}" from (${sql1})s${idx} where "${fields[0].name}"::text=q."${key}"::text limit 1) as "${key}_text","${key}"::text`;
    }
    return `"${key}"::text`;
  }));
  propsCache[cache] = propsCache[cache] || propsWithCls;

  const geomCol = (style?.type === 'point' ? `ST_Centroid(${geom})` : null)
    || (parseInt(z, 10) < parseInt(query.pointZoom || style?.pointZoom || style?.iconZoom || '0', 10) ? `ST_Centroid(${geom})` : geom);

  const koef = {
    10: 0.1, 11: 0.05, 12: 0.005, 13: 0.001, 14: 0.001, 15: 0.0005,
  }[z] || 0.0005;

  const types = { point: 'ST_Point' /* ,ST_MultiPoint */, polygon: 'ST_Polygon,ST_MultiPolygon' };
  const clusterZoom1 = style?.clusterZoom || clusterZoom;
  const queryMode = ((clusterZoom1 - z) > 2 ? 1 : null) || ((clusterZoom1 - z) > 0 ? 2 : null) || 3;
  // console.log(z, clusterZoom, queryMode);
  const props1 = props.filter((el, idx, arr) => idx === arr.findIndex(item => item.name === el.name));
  const q = {
    1: `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
            FROM (
                SELECT floor(random() * 100000 + 1)::int + row_number() over() as row, point_count, ST_AsMVTGeom(st_transform(geom,3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) geom
                FROM (
                SELECT st_centroid(st_union(geom)) as geom, count(*) as point_count
                FROM ${table} where ${layerQuery || 'true'} and ${filterData?.q || 'true'} and ${geom} && ${bbox2d}  ) j            
                )q`,
    2: `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
            FROM (
                SELECT floor(random() * 100000 + 1)::int +row_number() over() as row, count(*) as point_count,                
                ST_AsMVTGeom(st_transform(st_centroid(ST_Union(geom)),3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) as geom
                FROM (
                SELECT geom, ST_ClusterDBSCAN(geom,${koef},1) OVER () AS cluster 
                FROM ${table} where ${layerQuery || 'true'} and ${filterData?.q || 'true'} and ${geom} && ${bbox2d}  ) j
                WHERE cluster IS NOT NULL
                GROUP BY cluster
                ORDER BY 1 DESC
                )q`,
    3: `SELECT ST_AsMVT(q, '${layer}', 4096, 'geom','row') as tile
                FROM (
                    SELECT

                        floor(random() * 100000 + 1)::int + row_number() over() as row,

                        ${props?.length ? `${propsWithCls.join(',')},` : ''}

                        ${pkey} as  id, 
                        ST_AsMVTGeom(st_transform(${geomCol},3857),ST_TileEnvelope(${z},${y},${x})::box2d,4096,256,false) geom

                        FROM (select * from ${table} where ${layerQuery || 'true'} and ${filterData?.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 ${layerQuery || 'true'} and ${filterData?.q || 'true'} and ${data?.filter || '1=1'}
                    
                    ${(style?.type || data?.geometry_type) === 'polygon' ? `order by st_area(st_transform(${geom},3857)) desc` : ''}

                    limit 3000)q
                ) q`,
  }[queryMode];

  if (query.sql === '1') { return q; }
  const tile = await pg.query(q);
  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.headers({ ...headers, 'Cache-Control': nocache ? 'no-cache' : headers['Cache-Control'] }).send(buffer);
}
