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 {
    pgClients, getMeta,
} from '@opengis/fastify-table/utils.js';

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

const systemColumns = ['uid', 'cdate', 'editor_id', 'editor_date', 'created_by', 'created_at', 'updated_by', 'updated_at'];

/**
 * Повертає GeoJSON для сервісу на основі налаштувань з gis.services
 * 
 * @method GET
 * @param {string} params.layer - service_id
 * @param {string} query.sql - повернути SQL запит замість даних
 * @param {boolean} query.nocache - не використовувати кеш
 */
export default async function geojson({
    pg = pgClients.client, params = {}, query = {}, unittest,
}, reply) {
    const { layer } = params;
    const { sql, nocache, srid, type, extent } = query;

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

    const serviceId = layer.replace('.json', '');

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

    const service = await pg.query(
        `select 
            source_path, 
            query, 
            geometry_column, 
            attributes,
            srid,
            is_public,
            is_active,
            geom_type
        from gis.services 
        where service_id = $1 and is_active = true and is_public = true`,
        [serviceId],
    ).then(el => el.rows?.[0]);

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

    const {
        source_path: source,
        query: serviceQuery,
        geometry_column: gc,
        attributes,
        srid: serviceSrid,
        geom_type: geomType,
    } = service;

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

    const table = source;

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

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

    const geom = gc || meta?.geom || 'geom';
    const pk = pg.pk?.[table] || columns[0]?.name;

    let attributeColumns = [];
    if (attributes && Array.isArray(attributes)) {
        attributeColumns = attributes
            .filter(attr => attr.name && !systemColumns.includes(attr.name))
            .map(attr => attr.name);
    }

    const columnsFiltered = attributeColumns.length > 0
        ? columns.filter(col => attributeColumns.includes(col.name) && !systemColumns.includes(col.name))
        : [];

    const props = columnsFiltered.map(el =>
        `"${el.name}"${['int4'].includes(el.type) ? '::text' : ''}`
    ).join(', ');

    const geometryType = (geomType || 'point').toLowerCase();
    const limit = geometryType === 'point' ? 40000 : 2500;
    const cacheHash = createHash('md5')
        .update([serviceId, serviceQuery, srid, type, extent].filter(el => el).join('-'))
        .digest('hex');

    const today = new Date().toISOString().split('T')[0];
    const filepath = path.join(
        rootFolder,
        `/geojson/${serviceId}/${today}/${cacheHash}.json`,
    );

    if (existsSync(filepath) && !nocache && !sql && !unittest) {
        const { birthtimeMs = Date.now() } = await stat(filepath);
        const ageInMs = Date.now() - birthtimeMs;
        if (ageInMs < 86400000) {
            const content = await readFile(filepath, 'utf8');
            return reply
                .status(200)
                .header('Content-Type', 'application/json')
                .header('Cache-Control', 'public, max-age=86400')
                .send(content);
        }
    }

    const targetSrid = srid || serviceSrid || 4326;
    const useCentroid = type === 'centroid';

    let geomExpression = geom;

    if (useCentroid) {
        geomExpression = `st_centroid(${geomExpression})`;
    }

    if (targetSrid !== 4326) {
        geomExpression = `st_transform(${geomExpression}, 4326)`;
    } else if (serviceSrid && serviceSrid !== 4326) {
        geomExpression = `st_transform(${geomExpression}, 4326)`;
    }

    const quotedTable = table.split('.').map(el => `"${el}"`).join('.');
    const quotedGeom = `"${geom}"`;

    const geomFinal = `st_force2d(${geomExpression.replace(new RegExp(`(\\b)${geom}(\\b)`, 'g'), `$1${quotedGeom}$2`)})`;
    const q = `
        SELECT 'FeatureCollection' As type, 
               json_agg(f) As features
        FROM (
            SELECT 'Feature' As type,
                   row_number() over() as id,
                   st_asgeojson(${geomFinal}, 6, 0)::json as geometry,
                   (select row_to_json(tc) from (
                       select "${pk}" as id ${props ? ', ' + props : ''}
                   ) tc) as properties
            FROM ${quotedTable}
            WHERE ${serviceQuery || '1=1'}
              AND ${quotedGeom} is not null
              AND st_srid(${quotedGeom}) > 0
              ${extent ? `AND ${quotedGeom} && 'BOX(${extent.split(',').reduce((p, el, i) => p + el + (i % 2 ? ',' : ' '), '')})'::box2d` : ''}
            LIMIT ${limit}
        ) f
    `;

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

    try {
        const result = await pg.query(q);
        const geojsonData = result.rows[0] || { type: 'FeatureCollection', features: [] };

        geojsonData.service_id = serviceId;
        geojsonData.count = geojsonData.features?.length || 0;

        const jsonString = JSON.stringify(geojsonData);

        if (!nocache && !unittest) {
            await mkdir(path.dirname(filepath), { recursive: true });
            await writeFile(filepath, jsonString, 'utf8');
        }

        return reply
            .status(200)
            .header('Content-Type', 'application/json')
            .header('Cache-Control', nocache ? 'no-cache' : 'public, max-age=86400')
            .send(jsonString);
    } catch (err) {
        return reply.status(500).send({ error: err.message });
    }
}
