import { request as httpsRequest } from "node:https";

const amapHostname = "restapi.amap.com" as const;
const amapMaxResponseBytes = 65_536;
const amapTimeoutMs = 15_000;

export interface AmapHttpRequest {
  allowRedirects: false;
  hostname: typeof amapHostname;
  maxResponseBytes: number;
  method: "GET";
  path: string;
  protocol: "https:";
  rejectUnauthorized: true;
  timeoutMs: number;
}

type AmapRequester = (request: AmapHttpRequest) => Promise<unknown>;

export class AmapAdapterError extends Error {
  constructor(readonly code: "amap_adapter_disposed" | "amap_invalid_request" | "amap_invalid_response" | "amap_provider_rejected" | "amap_provider_unavailable" | "amap_redirect_rejected" | "amap_request_timeout" | "amap_response_too_large") {
    super(code);
  }
}

export interface AmapAdapter {
  dispose?(): void;
  reverseGeocode(coordinates: { latitude: number; longitude: number }): Promise<{ formattedValue: string; serviceMode: "mock" | "real" }>;
}

export class MockAmapAdapter implements AmapAdapter {
  readonly calls: Array<{ latitude: number; longitude: number }> = [];

  async reverseGeocode(coordinates: { latitude: number; longitude: number }) {
    this.calls.push({ ...coordinates });
    return {
      formattedValue: `模拟地点 ${coordinates.latitude.toFixed(4)}, ${coordinates.longitude.toFixed(4)}`,
      serviceMode: "mock" as const,
    };
  }
}

function requestAmapJson(input: AmapHttpRequest) {
  return new Promise<unknown>((resolve, reject) => {
    if (input.protocol !== "https:" || input.hostname !== amapHostname || input.allowRedirects || !input.rejectUnauthorized) {
      reject(new AmapAdapterError("amap_invalid_request"));
      return;
    }
    let settled = false;
    const finish = (callback: () => void) => {
      if (settled) return;
      settled = true;
      callback();
    };
    const request = httpsRequest({
      headers: { Accept: "application/json" },
      hostname: input.hostname,
      method: input.method,
      path: input.path,
      port: 443,
      protocol: input.protocol,
      rejectUnauthorized: input.rejectUnauthorized,
      servername: input.hostname,
    }, (response) => {
      const statusCode = response.statusCode ?? 0;
      if (statusCode >= 300 && statusCode < 400) {
        response.resume();
        finish(() => reject(new AmapAdapterError("amap_redirect_rejected")));
        return;
      }
      if (statusCode !== 200) {
        response.resume();
        finish(() => reject(new AmapAdapterError("amap_provider_unavailable")));
        return;
      }
      const declaredLength = Number(response.headers["content-length"] ?? 0);
      if (Number.isFinite(declaredLength) && declaredLength > input.maxResponseBytes) {
        response.destroy();
        finish(() => reject(new AmapAdapterError("amap_response_too_large")));
        return;
      }
      const chunks: Buffer[] = [];
      let receivedBytes = 0;
      response.on("data", (chunk: Buffer | string) => {
        const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
        receivedBytes += bytes.length;
        if (receivedBytes > input.maxResponseBytes) {
          response.destroy();
          finish(() => reject(new AmapAdapterError("amap_response_too_large")));
          return;
        }
        chunks.push(bytes);
      });
      response.on("end", () => {
        finish(() => {
          try {
            resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
          } catch {
            reject(new AmapAdapterError("amap_invalid_response"));
          } finally {
            for (const chunk of chunks) chunk.fill(0);
            chunks.length = 0;
          }
        });
      });
    });
    request.setTimeout(input.timeoutMs, () => request.destroy(new AmapAdapterError("amap_request_timeout")));
    request.on("error", (error) => finish(() => reject(error instanceof AmapAdapterError ? error : new AmapAdapterError("amap_provider_unavailable"))));
    request.end();
  });
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

export class RealAmapAdapter implements AmapAdapter {
  private readonly credential: Buffer;
  private readonly requester: AmapRequester;
  private disposed = false;

  constructor(value: string, options: { request?: AmapRequester } = {}) {
    if (!value.trim()) throw new AmapAdapterError("amap_invalid_request");
    this.credential = Buffer.from(value, "utf8");
    this.requester = options.request ?? requestAmapJson;
  }

  async reverseGeocode(coordinates: { latitude: number; longitude: number }) {
    if (this.disposed) throw new AmapAdapterError("amap_adapter_disposed");
    if (!Number.isFinite(coordinates.latitude) || coordinates.latitude < -90 || coordinates.latitude > 90
      || !Number.isFinite(coordinates.longitude) || coordinates.longitude < -180 || coordinates.longitude > 180) {
      throw new AmapAdapterError("amap_invalid_request");
    }
    const query = new URLSearchParams({
      extensions: "base",
      key: this.credential.toString("utf8"),
      location: `${coordinates.longitude},${coordinates.latitude}`,
    });
    const response = await this.requester({
      allowRedirects: false,
      hostname: amapHostname,
      maxResponseBytes: amapMaxResponseBytes,
      method: "GET",
      path: `/v3/geocode/regeo?${query.toString()}`,
      protocol: "https:",
      rejectUnauthorized: true,
      timeoutMs: amapTimeoutMs,
    });
    if (!isRecord(response) || response.status !== "1" || !isRecord(response.regeocode)) {
      throw new AmapAdapterError("amap_provider_rejected");
    }
    const formattedValue = typeof response.regeocode.formatted_address === "string"
      ? response.regeocode.formatted_address.trim()
      : "";
    if (!formattedValue || formattedValue.length > 200) throw new AmapAdapterError("amap_invalid_response");
    return { formattedValue, serviceMode: "real" as const };
  }

  dispose() {
    if (this.disposed) return;
    this.disposed = true;
    this.credential.fill(0);
  }
}
