<?php
// list_lookup.php  (with PID cache)
// --------------------------------------------------------------
ini_set('display_errors','0'); error_reporting(E_ALL);
set_error_handler(function($s,$m,$f,$l){ throw new ErrorException($m,0,$s,$f,$l); });
header('Content-Type: application/json; charset=utf-8');

define('LL_THROTTLE_US', 150000); // 150ms per request; set 0 to disable

// Convert PHP notices/warnings into exceptions -> JSON error
set_error_handler(function($severity, $message, $file, $line) {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

ob_start();

try {

    $raw = file_get_contents('php://input');
    $js  = json_decode($raw, true);
    if (!$js || !isset($js['lat'],$js['lng'])) {
        http_response_code(400);
        echo json_encode(['ok'=>false,'error'=>'Missing lat/lng']); exit;
    }
    $lat   = (float)$js['lat'];
    $lng   = (float)$js['lng'];
    $debug = !empty($js['debug']);
    $full  = !empty($js['full']) || !empty($js['debug']);

    // --- Small file cache ----------------------------------------------------
    define('LL_CACHE_DIR', __DIR__ . '/cache');   // adjust if you like
    define('LL_CACHE_TTL', 60 * 60 * 24 * 14);                      // 14 days

    function ll_cache_get($key){
        if (!is_dir(LL_CACHE_DIR)) return null;
        $f = LL_CACHE_DIR.'/'.preg_replace('/[^a-z0-9_.-]/i','_', $key).'.json';
        if (!is_file($f)) return null;
        if (filemtime($f) < time()-LL_CACHE_TTL) return null;
        $s = file_get_contents($f);
        $j = json_decode($s,true);
        return $j ?: null;
    }
    function ll_cache_set($key,$data){
        if (!is_dir(LL_CACHE_DIR)) @mkdir(LL_CACHE_DIR,0775,true);
        $f = LL_CACHE_DIR.'/'.preg_replace('/[^a-z0-9_.-]/i','_', $key).'.json';
        @file_put_contents($f, json_encode($data, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE));
    }

    // Pre-cache key by snapped lat/lng (so we can serve before we know PID)
    $snap = sprintf('%0.6f,%0.6f', $lat, $lng);
    $preKey = "v2-latlng-".$snap;
    if ($cached = ll_cache_get($preKey)) {
        if (!$full) unset($cached['raw']);
        echo json_encode($cached, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
        exit;
    }

    // ---------------- Config ----------------
    $service    = 'https://services.thelist.tas.gov.au/arcgis/rest/services/Public/PlanningOnline/MapServer';

    // ---------------- Helpers ----------------
    function arcgis_query($layerId, $lng, $lat, $outFields='*', $returnGeometry=false, $extraParams=[]) {
        global $service;
        $params = array_merge([
            'f'                => 'json',
            'where'            => '1=1',
            'returnGeometry'   => $returnGeometry ? 'true' : 'false',
            'outFields'        => $outFields,
            'outSR'            => '4326',
            'geometryType'     => 'esriGeometryPoint',
            'spatialRel'       => 'esriSpatialRelIntersects',
            'inSR'             => '4326',
            'geometry'         => json_encode(['x'=>(float)$lng, 'y'=>(float)$lat]),
            'resultRecordCount'=> 200
        ], $extraParams);

        $url = "{$service}/{$layerId}/query";
        $ch  = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => http_build_query($params),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 12,
        ]);
        $resp = curl_exec($ch);
        if ($resp === false) { $err = curl_error($ch); curl_close($ch); return ['error'=>"cURL error: $err"]; }
        curl_close($ch);
        if (LL_THROTTLE_US > 0) usleep(LL_THROTTLE_US);
        return json_decode($resp, true);
    }
    // --- Add this helper block ---
    function wmProject($lon, $lat) { // Web Mercator meters
        // clamp latitude to valid mercator range
        if ($lat > 85.05112878) $lat = 85.05112878;
        if ($lat < -85.05112878) $lat = -85.05112878;
        $originShift = 20037508.342789244;
        $x = $lon * $originShift / 180.0;
        $y = log(tan((90 + $lat) * M_PI / 360.0)) * $originShift / M_PI;
        return [$x, $y];
    }
    function ringAreaSignedSqm(array $ring) { // ring is [[lon,lat],...]
        $n = count($ring); if ($n < 3) return 0.0;
        $sum = 0.0;
        for ($i=0; $i<$n; $i++) {
            $j = ($i+1) % $n;
            [$xi,$yi] = wmProject($ring[$i][0], $ring[$i][1]);
            [$xj,$yj] = wmProject($ring[$j][0], $ring[$j][1]);
            $sum += ($xi * $yj) - ($xj * $yi);
        }
        return 0.5 * $sum; // signed
    }
    function polygonAreaFromRingsSqm(array $rings) {
        // Esri rings: outers vs holes by winding; summing signed areas handles holes
        $total = 0.0;
        foreach ($rings as $ring) $total += ringAreaSignedSqm($ring);
        return abs($total);
    }

    // ---------------- Layer IDs ----------------
    $LAYER_PARCELS = 2;   // Cadastral Parcels
    $LAYER_LGA     = 8;   // Local Government Areas
    $LAYER_ZONES   = 13;  // Tasm. Planning Scheme Zones
    $LAYER_CODES   = 14;  // Tasm. Planning Scheme Code Overlays

    // ---------------- Parcels (attributes) ----------------
    $parcelsResp = arcgis_query($LAYER_PARCELS, $lng, $lat, '*', false);
    $parcelFeat  = ($parcelsResp['features'][0] ?? null);
    $parcelAttr  = $parcelFeat['attributes'] ?? [];

    // PID (prefer numeric)
    $pid = null;
    foreach (['PID','PROPERTY_ID'] as $k) {
        if (!empty($parcelAttr[$k])) { $pid = preg_replace('/[^0-9]/','', (string)$parcelAttr[$k]); break; }
    }

    // ---------------- Parcel geometry (polygon) ----------------
    $parcelGeomResp = arcgis_query($LAYER_PARCELS, $lng, $lat, 'PID,OBJECTID', true);
    $boundaryGeoJSON = null;
    if (!empty($parcelGeomResp['features'][0]['geometry']['rings'])) {
        $rings = $parcelGeomResp['features'][0]['geometry']['rings']; // already [lng,lat] due to outSR=4326
        $boundaryGeoJSON = [
            'type' => 'Feature',
            'geometry' => [ 'type'=>'Polygon', 'coordinates'=>$rings ],
            'properties' => [ 'pid'=>$pid ]
        ];
    }

    // ---------------- Title (Volume/Folio) & Area ----------------
    $volume  = $parcelAttr['VOLUME'] ?? null;
    $folio   = isset($parcelAttr['FOLIO']) ? (string)$parcelAttr['FOLIO'] : null;
    $titleId = ($volume && $folio !== null && $folio !== '') ? ($volume . '/' . $folio)
        : ($parcelAttr['CT'] ?? $parcelAttr['CT_REFERENCE'] ?? null);

    // Area: prefer measured area, then computed
    $sqm = null;
    foreach (['MEAS_AREA','COMP_AREA','AREA_SQM','Shape__Area','SHAPE_Area'] as $k) {
        if (isset($parcelAttr[$k]) && is_numeric($parcelAttr[$k])) { $sqm = (float)$parcelAttr[$k]; break; }
    }
    // Fallback: compute from geometry rings if available
    if ($sqm === null && !empty($parcelGeomResp['features'][0]['geometry']['rings'])) {
        $sqm = polygonAreaFromRingsSqm($parcelGeomResp['features'][0]['geometry']['rings']);
    }

    $total_area = null; $area_sqm = null; $area_ha = null;
    if ($sqm !== null) {
        $area_sqm = (float)$sqm;
        $area_ha  = (float)$sqm / 10000.0;
        $total_area = [
            'sqm'       => $area_sqm,
            'sqm_label' => number_format($area_sqm, 0) . ' sqm',
            'ha'        => $area_ha,
            'ha_label'  => rtrim(rtrim(number_format($area_ha, 4, '.', ''), '0'), '.') . ' ha'
        ];
    }

    $tenure   = $parcelAttr['TENURE_TY'] ?? null;
    $lpi      = $parcelAttr['LPI'] ?? null;
    $listGuid = $parcelAttr['LIST_GUID'] ?? null;

    // ---------------- LGA ----------------
    $lgasResp = arcgis_query($LAYER_LGA, $lng, $lat, '*', false);
    $lgaAttr  = ($lgasResp['features'][0]['attributes'] ?? []);
    $council  = $lgaAttr['LGA_NAME'] ?? $lgaAttr['NAME'] ?? $lgaAttr['COUNCIL'] ?? null;

    // ---------------- Zones ----------------
    $zonesResp = arcgis_query($LAYER_ZONES, $lng, $lat, '*', false);
    $zoneNames = [];
    $schemeName = null;
    foreach (($zonesResp['features'] ?? []) as $zf) {
        $a = $zf['attributes'] ?? [];
        foreach (['ZONE','ZONING','ZONE_NAME','ZONE_LABEL'] as $k) {
            if (!empty($a[$k])) { $zoneNames[] = $a[$k]; break; }
        }
        if (!$schemeName) $schemeName = $a['LPS'] ?? $a['SCHEME'] ?? 'Tasmanian Planning Scheme';
    }
    $zoneNames = array_values(array_unique(array_filter($zoneNames)));

    // ---------------- Codes ----------------
    $codesResp = arcgis_query($LAYER_CODES, $lng, $lat, '*', false);
    $codeNames = [];
    foreach (($codesResp['features'] ?? []) as $cf) {
        $a = $cf['attributes'] ?? [];
        foreach (['CODE','OVERLAY','OVERLAY_CODE','CODE_NAME','OVERLAY_DESC'] as $k) {
            if (!empty($a[$k])) { $codeNames[] = $a[$k]; break; }
        }
    }
    $codeNames = array_values(array_unique(array_filter($codeNames)));

    // Build a compact raw attributes cache (no geometry except parcel boundary you already add)
    $raw = [
        'parcels' => array_values(array_map(
            fn($f) => $f['attributes'] ?? [],
            (array)($parcelsResp['features'] ?? [])
        )),
        'lga' => array_values(array_map(
            fn($f) => $f['attributes'] ?? [],
            (array)($lgasResp['features'] ?? [])
        )),
        'zones' => array_values(array_map(
            fn($f) => $f['attributes'] ?? [],
            (array)($zonesResp['features'] ?? [])
        )),
        'codes' => array_values(array_map(
            fn($f) => $f['attributes'] ?? [],
            (array)($codesResp['features'] ?? [])
        )),
    ];

    // ---------------- Output ----------------
    $out = [
        'ok'               => true,
        'pid'              => $pid,
        'title_id'         => $titleId,
        'tenure'           => $tenure,
        'lpi'              => $lpi,
        'list_guid'        => $listGuid,
        'total_area'       => $total_area,
        'area_sqm'         => $area_sqm,
        'area_ha'          => $area_ha,
        'council'          => $council,
        'planning_scheme'  => $schemeName,
        'planning_zones'   => $zoneNames,
        'planning_codes'   => $codeNames,
        'boundary'         => $boundaryGeoJSON
    ];

    if ($debug) $out['debug'] = ['parcel_attrs'=>$parcelAttr, 'lga_attrs'=>$lgaAttr];
    
    // Cache a “full” version that includes raw attributes
    $cacheOut = $out;
    $cacheOut['raw'] = $raw;

    // Cache under snapped lat/lng immediately…
    ll_cache_set($preKey, $out);
    // …and if we have a PID, also cache under PID (best key)
    if ($pid) ll_cache_set('v2-pid-'.$pid, $cacheOut);

    // Return small by default; include raw only if requested
    $responseOut = $out;
    if ($full) $responseOut['raw'] = $raw;

    echo json_encode($responseOut, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);

} catch (Throwable $e) {
    http_response_code(500);
    echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
}