// Thin server-side client for api.gis.ac (Plan 003, Faza D).
//
// SERVER-SIDE ONLY. Never import from a "use client" component — token
// extraction depends on NextAuth getServerSession + Authentik access_token
// exposed by auth-options.ts. Browser code must call internal ArchiTools
// API routes that wrap gisApi.* on the server.
//
// All methods auto-extract the Authentik access_token from the current
// request's NextAuth session. Pass { accessToken } in opts to override
// (only useful for service-account contexts, e.g. background jobs).
//
// Endpoint table + scope semantics: docs/plans/003-architools-cutover-
// execution-2026-05-17.md §Faza D.

import { getServerSession } from "next-auth";
import { authOptions } from "@/core/auth/auth-options";

const GIS_API_URL = process.env.GIS_API_URL || "https://api.gis.ac";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export type EnrichmentScope = "none" | "basic" | "full";

export interface AuthClaims {
  sub: string;
  tenant: string;
  org_ids: string[];
  is_beletage_group: boolean;
  enrichment_scope: EnrichmentScope;
  email?: string;
}

export interface ParcelRefBody {
  siruta: string;
  cadastralRef: string;
  force?: boolean;
}

export interface ImmAppsBody extends ParcelRefBody {
  layerId?: "TERENURI_ACTIVE" | "CLADIRI_ACTIVE";
}

export interface CfExtractRow {
  id: string;
  userId: string;
  nrCadastral: string;
  nrCF?: string;
  status:
    | "pending"
    | "queued"
    | "cart"
    | "ordering"
    | "polling"
    | "downloading"
    | "completed"
    | "failed"
    | "cancelled";
  epayStatus?: string;
  orderId?: string;
  basketRowId?: string;
  documentName?: string;
  documentDate?: string;
  minioPath?: string;
  completedAt?: string;
  expiresAt?: string;
  createdAt: string;
  updatedAt: string;
}

export interface CfListResponse {
  total: number;
  limit: number;
  offset: number;
  rows: CfExtractRow[];
}

export interface RateLimit {
  limit: number;
  remaining: number;
  resetSec: number;
}

// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------

export class GisApiError extends Error {
  constructor(
    public status: number,
    public code: string,
    public body?: unknown,
    public rateLimit?: RateLimit,
  ) {
    super(`${code} (HTTP ${status})`);
    this.name = "GisApiError";
  }
}

// ---------------------------------------------------------------------------
// Internal request helper
// ---------------------------------------------------------------------------

interface RequestOpts {
  accessToken?: string;
  method?: string;
  body?: BodyInit | null;
  headers?: Record<string, string>;
  query?: Record<string, string | number | undefined | null>;
}

async function bearerFromSession(): Promise<string> {
  const session = await getServerSession(authOptions);
  const tok = (session as { accessToken?: string } | null)?.accessToken;
  if (!tok) {
    throw new GisApiError(401, "no_session_access_token", {
      hint: "User not signed in or NextAuth session has no Authentik access_token",
    });
  }
  return tok;
}

function buildUrl(path: string, query?: RequestOpts["query"]): string {
  const url = new URL(`${GIS_API_URL}${path}`);
  if (query) {
    for (const [k, v] of Object.entries(query)) {
      if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
    }
  }
  return url.toString();
}

function parseRateLimit(res: Response): RateLimit | undefined {
  const limit = res.headers.get("X-RateLimit-Limit");
  const remaining = res.headers.get("X-RateLimit-Remaining");
  const reset = res.headers.get("X-RateLimit-Reset");
  if (!limit) return undefined;
  return {
    limit: Number(limit),
    remaining: Number(remaining) || 0,
    resetSec: Number(reset) || 0,
  };
}

// Default per-request timeout. Light queries (search, parcela.get) usually
// finish in <1s; proxy calls (parcel/tech) hit ANCPI live and can take 5-15s.
const DEFAULT_TIMEOUT_MS = 30_000;

async function request<T = unknown>(path: string, opts: RequestOpts = {}): Promise<T> {
  const token = opts.accessToken || (await bearerFromSession());
  let res: Response;
  try {
    res = await fetch(buildUrl(path, opts.query), {
      method: opts.method || "GET",
      body: opts.body ?? null,
      headers: {
        Authorization: `Bearer ${token}`,
        Accept: "application/json",
        ...opts.headers,
      },
      cache: "no-store",
      signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
    });
  } catch (err) {
    const isAbort = (err as { name?: string })?.name === "TimeoutError" ||
      (err as { name?: string })?.name === "AbortError";
    throw new GisApiError(
      504,
      isAbort ? "upstream_timeout" : "upstream_unreachable",
      { hint: (err as Error)?.message },
    );
  }

  const rateLimit = parseRateLimit(res);

  if (!res.ok) {
    let body: unknown = null;
    try {
      body = await res.json();
    } catch {
      body = await res.text();
    }
    const code = (body as { error?: string })?.error || `gis_api_${res.status}`;
    throw new GisApiError(res.status, code, body, rateLimit);
  }

  // Streaming endpoints (PDF) need raw Response — caller decides
  if (res.headers.get("Content-Type")?.startsWith("application/pdf")) {
    return res as unknown as T;
  }
  return (await res.json()) as T;
}

async function rawResponse(path: string, opts: RequestOpts = {}): Promise<Response> {
  const token = opts.accessToken || (await bearerFromSession());
  let res: Response;
  try {
    res = await fetch(buildUrl(path, opts.query), {
      method: opts.method || "GET",
      body: opts.body ?? null,
      headers: {
        Authorization: `Bearer ${token}`,
        ...opts.headers,
      },
      cache: "no-store",
      signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
    });
  } catch (err) {
    const isAbort = (err as { name?: string })?.name === "TimeoutError" ||
      (err as { name?: string })?.name === "AbortError";
    throw new GisApiError(
      504,
      isAbort ? "upstream_timeout" : "upstream_unreachable",
      { hint: (err as Error)?.message },
    );
  }
  if (!res.ok) {
    let body: unknown = null;
    try {
      body = await res.clone().json();
    } catch {
      body = await res.clone().text();
    }
    const code = (body as { error?: string })?.error || `gis_api_${res.status}`;
    throw new GisApiError(res.status, code, body, parseRateLimit(res));
  }
  return res;
}

// ---------------------------------------------------------------------------
// Public surface
// ---------------------------------------------------------------------------

export interface GisApiCallOpts {
  accessToken?: string;
}

export const gisApi = {
  me: (opts: GisApiCallOpts = {}) =>
    request<{ claims: AuthClaims }>("/api/v1/me", { accessToken: opts.accessToken }),

  parcela: {
    get: (id: string, opts: GisApiCallOpts = {}) =>
      request<unknown>(`/api/v1/parcela/${encodeURIComponent(id)}`, {
        accessToken: opts.accessToken,
      }),
  },

  search: (q: string, limit = 20, opts: GisApiCallOpts = {}) =>
    request<{
      q: string;
      uats: Array<{ siruta: string; name: string; county: string }>;
      features: Array<{
        id: string;
        layerId: string;
        cadastralRef: string;
        areaValue?: number;
      }>;
    }>("/api/v1/search", {
      query: { q, limit },
      accessToken: opts.accessToken,
    }),

  parcel: {
    tech: (body: ParcelRefBody, opts: GisApiCallOpts = {}) =>
      request<unknown>("/api/v1/parcel/tech", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        accessToken: opts.accessToken,
      }),
    unitsFetch: (body: ParcelRefBody, opts: GisApiCallOpts = {}) =>
      request<unknown>("/api/v1/parcel/units/fetch", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        accessToken: opts.accessToken,
      }),
    immApps: (body: ImmAppsBody, opts: GisApiCallOpts = {}) =>
      request<unknown>("/api/v1/parcel/imm-apps", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        accessToken: opts.accessToken,
      }),
  },

  building: {
    tech: (body: ParcelRefBody, opts: GisApiCallOpts = {}) =>
      request<unknown>("/api/v1/building/tech", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        accessToken: opts.accessToken,
      }),
    condoOwners: (body: ParcelRefBody, opts: GisApiCallOpts = {}) =>
      request<unknown>("/api/v1/building/condo-owners", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
        accessToken: opts.accessToken,
      }),
  },

  enrichment: {
    cf: {
      list: (
        params: { limit?: number; offset?: number; status?: string } = {},
        opts: GisApiCallOpts = {},
      ) =>
        request<CfListResponse>("/api/v1/enrichment/cf", {
          query: params,
          accessToken: opts.accessToken,
        }),
      get: (id: string, opts: GisApiCallOpts = {}) =>
        request<CfExtractRow>(`/api/v1/enrichment/cf/${encodeURIComponent(id)}`, {
          accessToken: opts.accessToken,
        }),
      create: (
        body: { nrCadastral: string; siruta?: string; gisFeatureId?: string },
        opts: GisApiCallOpts = {},
      ) =>
        request<CfExtractRow | { error: "catalog_hit"; existing: CfExtractRow }>(
          "/api/v1/enrichment/cf",
          {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
            accessToken: opts.accessToken,
          },
        ),
      patch: (id: string, body: Partial<CfExtractRow>, opts: GisApiCallOpts = {}) =>
        request<CfExtractRow>(`/api/v1/enrichment/cf/${encodeURIComponent(id)}`, {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
          accessToken: opts.accessToken,
        }),
      uploadPdf: (
        id: string,
        pdf: ArrayBuffer | Uint8Array | Blob,
        opts: GisApiCallOpts & { documentName?: string } = {},
      ) =>
        request<{ status: "ok"; size: number }>(
          `/api/v1/enrichment/cf/${encodeURIComponent(id)}/pdf`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/pdf",
              ...(opts.documentName
                ? { "X-Document-Name": opts.documentName }
                : {}),
            },
            body: pdf as BodyInit,
            accessToken: opts.accessToken,
          },
        ),
      // Returns raw Response — caller streams to client (PDF body)
      getPdf: (id: string, opts: GisApiCallOpts = {}) =>
        rawResponse(`/api/v1/enrichment/cf/${encodeURIComponent(id)}/pdf`, {
          accessToken: opts.accessToken,
        }),
    },
    catalog: (nrCadastral: string, opts: GisApiCallOpts = {}) =>
      request<{
        nrCadastral: string;
        siruta?: string;
        uatName?: string;
        completedAt?: string;
        expiresAt?: string;
        isFresh: boolean;
      }>(`/api/v1/enrichment/catalog/${encodeURIComponent(nrCadastral)}`, {
        accessToken: opts.accessToken,
      }),
  },
};
