export type SSEMethod = "GET" | "POST" | "PUT" | "DELETE";

export interface SSEOptions {
  headers?: Record<string, string>;
  method?: SSEMethod;
  payload?: string;
  withCredentials?: boolean;
  timeout?: number;
}

type SSEOpenHandler = ((event?: Event) => void) | null;
type SSEMessageHandler = ((event: MessageEvent<string>) => void) | null;
type SSEErrorHandler = ((error: unknown) => void) | null;
type ByteArray = Uint8Array<ArrayBufferLike>;

type ChunkDecoder = {
  decode: (data: unknown) => string;
  flush: () => string;
};

function toUint8Array(data: unknown): ByteArray | null {
  if (data instanceof Uint8Array) {
    return data as ByteArray;
  }
  if (data instanceof ArrayBuffer) {
    return new Uint8Array(data) as ByteArray;
  }
  if (ArrayBuffer.isView(data)) {
    return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) as ByteArray;
  }
  return null;
}

function decodeUtf8Bytes(bytes: ByteArray, previousTail: ByteArray): { text: string; tail: ByteArray } {
  const merged = previousTail.length
    ? (() => {
      const tmp = new Uint8Array(previousTail.length + bytes.length);
      tmp.set(previousTail, 0);
      tmp.set(bytes, previousTail.length);
      return tmp as ByteArray;
    })()
    : bytes;

  let text = "";
  let i = 0;

  while (i < merged.length) {
    const b1 = merged[i];

    if (b1 <= 0x7f) {
      text += String.fromCharCode(b1);
      i += 1;
      continue;
    }

    let needed = 0;
    if (b1 >= 0xc2 && b1 <= 0xdf) needed = 2;
    else if (b1 >= 0xe0 && b1 <= 0xef) needed = 3;
    else if (b1 >= 0xf0 && b1 <= 0xf4) needed = 4;
    else {
      text += "\ufffd";
      i += 1;
      continue;
    }

    if (i + needed > merged.length) {
      break;
    }

    const b2 = merged[i + 1];
    if ((b2 & 0xc0) !== 0x80) {
      text += "\ufffd";
      i += 1;
      continue;
    }

    if (needed === 2) {
      const codePoint = ((b1 & 0x1f) << 6) | (b2 & 0x3f);
      text += String.fromCharCode(codePoint);
      i += 2;
      continue;
    }

    const b3 = merged[i + 2];
    if ((b3 & 0xc0) !== 0x80) {
      text += "\ufffd";
      i += 1;
      continue;
    }

    if (needed === 3) {
      const codePoint = ((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f);
      text += String.fromCharCode(codePoint);
      i += 3;
      continue;
    }

    const b4 = merged[i + 3];
    if ((b4 & 0xc0) !== 0x80) {
      text += "\ufffd";
      i += 1;
      continue;
    }

    const codePoint =
      ((b1 & 0x07) << 18) |
      ((b2 & 0x3f) << 12) |
      ((b3 & 0x3f) << 6) |
      (b4 & 0x3f);
    const cp = codePoint - 0x10000;
    text += String.fromCharCode((cp >> 10) + 0xd800, (cp & 0x3ff) + 0xdc00);
    i += 4;
  }

  return {
    text,
    tail: i < merged.length ? (merged.slice(i) as ByteArray) : (new Uint8Array(0) as ByteArray),
  };
}

function createChunkDecoder(): ChunkDecoder {
  if (typeof TextDecoder !== "undefined") {
    const decoder = new TextDecoder("utf-8");
    return {
      decode(data: unknown) {
        if (typeof data === "string") {
          return data;
        }
        const bytes = toUint8Array(data);
        if (!bytes) {
          return "";
        }
        return decoder.decode(bytes, { stream: true });
      },
      flush() {
        return decoder.decode();
      },
    };
  }

  let tail: ByteArray = new Uint8Array(0) as ByteArray;
  return {
    decode(data: unknown) {
      if (typeof data === "string") {
        return data;
      }
      const bytes = toUint8Array(data);
      if (!bytes) {
        return "";
      }
      const decoded = decodeUtf8Bytes(bytes, tail);
      tail = decoded.tail;
      return decoded.text;
    },
    flush() {
      if (!tail.length) {
        return "";
      }
      const decoded = decodeUtf8Bytes(new Uint8Array(0) as ByteArray, tail);
      tail = new Uint8Array(0) as ByteArray;
      return decoded.text;
    },
  };
}

function parsePayload(payload?: string): string | Record<string, unknown> | ArrayBuffer | undefined {
  if (!payload) {
    return undefined;
  }
  try {
    return JSON.parse(payload);
  } catch {
    return payload;
  }
}

export class SSE {
  onopen: SSEOpenHandler = null;
  onmessage: SSEMessageHandler = null;
  onerror: SSEErrorHandler = null;

  private requestTask: UniApp.RequestTask | null = null;
  private abortController: AbortController | null = null;
  private buffer = "";
  private lastEventId = "";
  private closed = false;
  private opened = false;

  constructor(
    private readonly url: string,
    private readonly options: SSEOptions = {},
  ) {}

  stream() {
    this.closed = false;
    this.opened = false;
    this.buffer = "";

    if (typeof uni !== "undefined" && typeof uni.request === "function") {
      this.startWithUniRequest();
      return;
    }

    this.startWithFetch();
  }

  close() {
    this.closed = true;

    if (this.requestTask) {
      this.requestTask.abort();
      this.requestTask = null;
    }

    if (this.abortController) {
      this.abortController.abort();
      this.abortController = null;
    }
  }

  private emitOpen() {
    if (this.opened || this.closed) {
      return;
    }
    this.opened = true;
    this.onopen?.();
  }

  private emitError(error: unknown) {
    this.onerror?.(error);
    if (this.closed) {
      return;
    }
  }

  private dispatchRawEvent(raw: string) {
    if (!raw.trim()) {
      return;
    }

    const lines = raw.split("\n");
    const dataLines: string[] = [];
    let eventName = "message";

    for (const line of lines) {
      if (!line || line.startsWith(":")) {
        continue;
      }

      const separator = line.indexOf(":");
      const field = separator >= 0 ? line.slice(0, separator) : line;
      let value = separator >= 0 ? line.slice(separator + 1) : "";
      if (value.startsWith(" ")) {
        value = value.slice(1);
      }

      if (field === "data") {
        dataLines.push(value);
      } else if (field === "event" && value) {
        eventName = value;
      } else if (field === "id") {
        this.lastEventId = value;
      }
    }

    if (dataLines.length === 0) {
      return;
    }

    const event = {
      data: dataLines.join("\n"),
      type: eventName,
      lastEventId: this.lastEventId,
      origin: this.url,
    } as MessageEvent<string>;

    // 兼容 Chat.ts 的使用方式：统一通过 onmessage 消费数据。
    this.onmessage?.(event);
  }

  private consumeSseChunk(chunkText: string) {
    if (!chunkText || this.closed) {
      return;
    }

    this.emitOpen();
    this.buffer += chunkText.replace(/\r\n/g, "\n").replace(/\r/g, "\n");

    let boundaryIndex = this.buffer.indexOf("\n\n");
    while (boundaryIndex >= 0) {
      const rawEvent = this.buffer.slice(0, boundaryIndex);
      this.buffer = this.buffer.slice(boundaryIndex + 2);
      this.dispatchRawEvent(rawEvent);
      boundaryIndex = this.buffer.indexOf("\n\n");
    }
  }

  private flushRemainder() {
    if (this.buffer.trim()) {
      this.dispatchRawEvent(this.buffer);
      this.buffer = "";
    }
  }

  private startWithUniRequest() {
    const chunkDecoder = createChunkDecoder();
    const method = this.options.method || "GET";
    const payloadData = parsePayload(this.options.payload);
    let hasReceivedChunk = false;

    const requestTask = uni.request({
      url: this.url,
      method,
      header: this.options.headers,
      data: payloadData,
      timeout: this.options.timeout,
      responseType: "arraybuffer",
      enableChunked: true,
      success: (res) => {
        if (res.statusCode !== 200) {
          this.emitError(new Error(
            res.data ? JSON.stringify(res.data) : `请求失败: ${res.statusCode}`
          ));
          return;
        }
        if (this.closed) {
          return;
        }
        if (!hasReceivedChunk) {
          const text = chunkDecoder.decode(res.data);
          this.consumeSseChunk(text);
        }
        const tail = chunkDecoder.flush();
        if (tail) {
          this.consumeSseChunk(tail);
        }
        this.flushRemainder();
      },
      fail: (error) => {
        this.emitError(error);
      },
    } as UniApp.RequestOptions);

    this.requestTask = requestTask as unknown as UniApp.RequestTask;

    const chunkableTask = this.requestTask as UniApp.RequestTask & {
      onChunkReceived?: (callback: (result: { data: ArrayBuffer }) => void) => void;
    };
    if (typeof chunkableTask.onChunkReceived === "function") {
      chunkableTask.onChunkReceived((result: { data: ArrayBuffer }) => {
        hasReceivedChunk = true;
        if (this.closed) {
          return;
        }
        const text = chunkDecoder.decode(result.data);
        this.consumeSseChunk(text);
      });
    }
  }

  private async startWithFetch() {
    if (typeof fetch === "undefined") {
      this.emitError(new Error("当前环境不支持 fetch 流式读取"));
      return;
    }

    this.abortController = new AbortController();
    const method = this.options.method || "GET";

    try {
      const response = await fetch(this.url, {
        method,
        headers: this.options.headers,
        body: this.options.payload,
        credentials: this.options.withCredentials ? "include" : "omit",
        signal: this.abortController.signal,
      });

      if (this.closed) {
        return;
      }

      if (!response.ok) {
        throw new Error(`请求失败: ${response.status} ${response.statusText}`);
      }

      this.emitOpen();

      if (!response.body) {
        const plainText = await response.text();
        this.consumeSseChunk(plainText);
        this.flushRemainder();
        return;
      }

      const reader = response.body.getReader();
      const chunkDecoder = createChunkDecoder();

      while (!this.closed) {
        const result = await reader.read();
        if (result.done) {
          break;
        }
        const chunkText = chunkDecoder.decode(result.value);
        this.consumeSseChunk(chunkText);
      }

      const tail = chunkDecoder.flush();
      if (tail) {
        this.consumeSseChunk(tail);
      }
      this.flushRemainder();
    } catch (error) {
      if (this.closed) {
        return;
      }
      this.emitError(error);
    }
  }
}
