import { BadRequestException, Body, Controller, Get, HttpCode, Logger, Post, Query, Req, Res, Sse } from "@nestjs/common";
import type { Response as ExpressResponse } from "express";
import { Observable } from "rxjs";
import { AmfProxyService } from "./amf-proxy.service.js";

type MessageEventLike = { data: unknown; type?: string };

@Controller()
export class AmfProxyController {
  private readonly logger = new Logger(AmfProxyController.name);

  constructor(private readonly amf: AmfProxyService) {}

  @Get("/up")
  up() {
    return { ok: true };
  }

  @Get("/leaderboard")
  async leaderboard(
    @Query("session_key") sessionKey?: string,
    @Query("char_id") charIdStr?: string,
    @Query("category") category?: string,
    @Query("url") url?: string
  ) {
    const key = String(sessionKey || "").trim();
    const categoryKey = String(category || "").trim();
    const charId = Number.parseInt(String(charIdStr || ""), 10);

    if (!key) throw new BadRequestException("session_key_required");
    if (!categoryKey) throw new BadRequestException("category_required");
    if (!Number.isFinite(charId)) throw new BadRequestException("char_id_required");

    return await this.amf.fetchLeaderboard({ charId, sessionKey: key, category: categoryKey, url });
  }

  @Get("/leaderboard/christmas")
  async leaderboardChristmas(
    @Query("session_key") sessionKey?: string,
    @Query("char_id") charIdStr?: string,
    @Query("url") url?: string
  ) {
    const charId = Number.parseInt(String(charIdStr || "173793"), 10);
    return await this.leaderboard(sessionKey, String(charId), "xmass2025", url);
  }

  @Sse("/leaderboard/christmas/stream")
  leaderboardChristmasStream(
    @Query("session_key") sessionKey?: string,
    @Query("char_id") charIdStr?: string,
    @Query("interval_ms") intervalMsStr?: string,
    @Query("url") url?: string
  ): Observable<MessageEventLike> {
    const key = String(sessionKey || "").trim();
    const charId = Number.parseInt(String(charIdStr || "173793"), 10);
    const intervalMs = Number.parseInt(String(intervalMsStr || "5000"), 10);
    if (!key) throw new BadRequestException("session_key_required");
    if (!Number.isFinite(charId)) throw new BadRequestException("char_id_required");
    return this.amf.leaderboardStream({ charId, sessionKey: key, category: "xmass2025", intervalMs, url });
  }

  @Post("/systemlogin/getAllCharacters")
  async systemLoginGetAllCharacters(@Body() body: any, @Query("url") url?: string) {
    let payload = body;
    if (Buffer.isBuffer(body)) {
      try {
        payload = JSON.parse(body.toString());
      } catch (e) {
        // Not JSON
      }
    }

    const { user_id, session_key } = payload;
    if (!user_id || !session_key) {
      throw new BadRequestException("user_id and session_key required");
    }
    return await this.amf.fetchSystemLoginGetAllCharacters({
      userId: user_id,
      sessionKey: session_key,
      url,
    });
  }

  @Post("/systemlogin/loginUser")
  async systemLoginLoginUser(@Body() body: any, @Query("url") url?: string) {
    let payload = body;
    if (Buffer.isBuffer(body)) {
      try {
        payload = JSON.parse(body.toString());
      } catch (e) {
        // Not JSON
      }
    }

    const { username, password_hash, token, signature, timestamp } = payload;
    if (!username || !password_hash) {
      throw new BadRequestException("username and password_hash required");
    }
    return await this.amf.fetchSystemLoginLoginUser({
      username,
      passwordHash: password_hash,
      token,
      signature,
      timestamp,
      url,
    });
  }

  @Post("/amf/proxy")
  async proxy(@Req() req: any, @Res() res: ExpressResponse, @Query("url") url?: string) {
    const resolved = this.amf.resolveUpstream(url);
    const headers = this.amf.buildUpstreamHeaders(req.headers || {}, resolved.host);
    const body: Uint8Array = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || "");

    try {
      const upstream = await this.amf.fetchUpstream(resolved.url, headers, body);
      const buf = Buffer.from(await upstream.arrayBuffer());
      res.status(upstream.status);
      res.setHeader("Cache-Control", "no-store");
      res.setHeader("Access-Control-Allow-Origin", "*");
      res.setHeader("Content-Type", upstream.headers.get("content-type") || "application/octet-stream");
      res.send(buf);
    } catch (err: any) {
      res.status(502).json({ error: "upstream_error", message: String(err?.message || err) });
    }
  }

  @Post("/amf/decode")
  @HttpCode(200)
  decode(@Req() req: any) {
    const body: Uint8Array = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || "");
    if (!body || body.length === 0) throw new BadRequestException("payload_required");
    try {
      return this.amf.decodeRemotingEnvelope(Buffer.from(body));
    } catch (e: any) {
      return {
        error: "decode_error",
        message: String(e?.message || e),
        bytes_len: Buffer.from(body).length,
      };
    }
  }

  @Post("/amf/roundtrip")
  async roundtrip(@Req() req: any, @Query("url") url?: string) {
    const resolved = this.amf.resolveUpstream(url);
    const headers = this.amf.buildUpstreamHeaders(req.headers || {}, resolved.host);
    const body: Uint8Array = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || "");
    if (!body || body.length === 0) throw new BadRequestException("payload_required");

    let requestDecoded: unknown = null;
    try {
      requestDecoded = this.amf.decodeRemotingEnvelope(Buffer.from(body));
    } catch (e: any) {
      requestDecoded = { error: "decode_error", message: String(e?.message || e) };
    }

    const upstream = await this.amf.fetchUpstream(resolved.url, headers, body);
    const respBuf = Buffer.from(await upstream.arrayBuffer());

    let responseDecoded: unknown = null;
    try {
      responseDecoded = this.amf.decodeRemotingEnvelope(respBuf);
    } catch (e: any) {
      responseDecoded = { error: "decode_error", message: String(e?.message || e) };
    }

    const sessionKey = this.amf.extractSessionKey(responseDecoded);

    return {
      upstream: {
        url: resolved.url,
        status: upstream.status,
        content_type: upstream.headers.get("content-type") || "",
      },
      request: requestDecoded,
      response: responseDecoded,
      extracted: {
        sessionkey: sessionKey,
      },
    };
  }

  @Sse("/amf/proxy/stream")
  stream(@Req() req: any, @Query("url") url?: string, @Query("payload_b64") payloadB64?: string): Observable<MessageEventLike> {
    return new Observable<MessageEventLike>((sub) => {
      const abort = new AbortController();
      const onClose = () => abort.abort();
      req.on?.("close", onClose);
      req.on?.("aborted", onClose);

      (async () => {
        let resolvedUrl = "";
        try {
          const resolved = this.amf.resolveUpstream(url);
          resolvedUrl = resolved.url;
          const headers = this.amf.buildUpstreamHeaders(req.headers || {}, resolved.host);

          let payload = Buffer.alloc(0);
          if (payloadB64) {
            try {
              payload = Buffer.from(String(payloadB64), "base64");
            } catch {
              sub.next({ type: "error", data: { error: "bad_payload_b64" } });
              sub.complete();
              return;
            }
          }

          const upstream = await this.amf.fetchUpstream(resolved.url, headers, payload, abort.signal);

          sub.next({
            type: "meta",
            data: { status: upstream.status, content_type: upstream.headers.get("content-type") || "" },
          });

          let total = 0;
          const maxBytes = this.amf.getMaxBytes();

          const reader = upstream.body?.getReader?.();
          if (!reader) {
            const buf = Buffer.from(await upstream.arrayBuffer());
            total = buf.length;
            sub.next({ type: "chunk", data: { b64: buf.toString("base64") } });
            sub.next({ type: "done", data: { bytes: total } });
            sub.complete();
            return;
          }

          while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            if (!value || value.length === 0) continue;

            total += value.length;
            if (maxBytes > 0 && total > maxBytes) {
              sub.next({ type: "error", data: { error: "max_bytes_exceeded", max_bytes: maxBytes } });
              break;
            }

            sub.next({ type: "chunk", data: { b64: Buffer.from(value).toString("base64") } });
          }

          sub.next({ type: "done", data: { bytes: total } });
          sub.complete();
        } catch (err: any) {
          sub.next({
            type: "error",
            data: { error: "upstream_error", message: String(err?.message || err), url: resolvedUrl },
          });
          sub.complete();
        }
      })();

      return () => {
        abort.abort();
        req.off?.("close", onClose);
        req.off?.("aborted", onClose);
      };
    });
  }

  @Sse("/systemlogin/auto/stream")
  systemLoginAutoStream(
    @Req() req: any,
    @Query("url") url?: string,
    @Query("cfg_b64") cfgB64?: string
  ): Observable<MessageEventLike> {
    return new Observable<MessageEventLike>((sub) => {
      const abort = new AbortController();
      const onClose = () => abort.abort();
      req.on?.("close", onClose);
      req.on?.("aborted", onClose);

      const nowSec = () => Math.floor(Date.now() / 1000);

      const safeParseCfg = (): any => {
        if (!cfgB64) return {};
        try {
          const json = Buffer.from(String(cfgB64), "base64").toString("utf8");
          const parsed = JSON.parse(json);
          return parsed && typeof parsed === "object" ? parsed : {};
        } catch {
          throw new BadRequestException("bad_cfg_b64");
        }
      };

      const extractByKey = (obj: unknown, key: string): unknown => {
        const want = key.toLowerCase();
        const seen = new Set<unknown>();
        const walk = (v: unknown): unknown => {
          if (v === null || v === undefined) return null;
          if (typeof v !== "object") return null;
          if (seen.has(v)) return null;
          seen.add(v);
          if (Array.isArray(v)) {
            for (const it of v) {
              const found = walk(it);
              if (found !== null && found !== undefined) return found;
            }
            return null;
          }
          for (const [k, vv] of Object.entries(v as Record<string, unknown>)) {
            if (k.toLowerCase() === want) return vv;
            const found = walk(vv);
            if (found !== null && found !== undefined) return found;
          }
          return null;
        };
        return walk(obj);
      };

      const extractFirstCharId = (obj: unknown): number | null => {
        const seen = new Set<unknown>();
        const walk = (v: unknown): number | null => {
          if (v === null || v === undefined) return null;
          if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
          if (typeof v === "string" && /^\d+$/.test(v)) return Number(v);
          if (typeof v !== "object") return null;
          if (seen.has(v)) return null;
          seen.add(v);
          if (Array.isArray(v)) {
            for (const it of v) {
              const found = walk(it);
              if (found) return found;
            }
            return null;
          }
          const rec = v as Record<string, unknown>;
          const maybeId = rec.char_id ?? rec.charId ?? rec.id ?? rec.character_id ?? rec.characterId;
          if (typeof maybeId === "number" && Number.isFinite(maybeId) && maybeId > 0) return maybeId;
          if (typeof maybeId === "string" && /^\d+$/.test(maybeId)) return Number(maybeId);
          for (const vv of Object.values(rec)) {
            const found = walk(vv);
            if (found) return found;
          }
          return null;
        };
        return walk(obj);
      };

      (async () => {
        let resolvedUrl = "";
        try {
          const cfg = safeParseCfg();

          const resolved = this.amf.resolveUpstream(url);
          resolvedUrl = resolved.url;
          const headers = this.amf.buildUpstreamHeaders(req.headers || {}, resolved.host);

          const username = String(cfg.username || "").trim();
          const passwordB64 = String(cfg.password_b64 || "").trim();
          const ts = Number.isFinite(cfg.ts) ? Number(cfg.ts) : nowSec();
          const n1 = Number.isFinite(cfg.n1) ? Number(cfg.n1) : 8101532;
          const n2 = Number.isFinite(cfg.n2) ? Number(cfg.n2) : 8101532;
          const s1 = String(cfg.s1 ?? "").trim();
          const s2 = String(cfg.s2 ?? "").trim();
          const s3 = String(cfg.s3 ?? "").trim();
          const v = Number.isFinite(cfg.v) ? Number(cfg.v) : 10;

          this.logger.log(`systemlogin auto start user=${username || "-"} upstream=${resolvedUrl}`);

          sub.next({
            type: "meta",
            data: {
              upstream: resolvedUrl,
              started_at: new Date().toISOString(),
            },
          });

          const missing: string[] = [];
          if (!username) missing.push("username");
          if (!passwordB64) missing.push("password_b64");
          if (!s1) missing.push("s1");
          if (!s2) missing.push("s2");
          if (!s3) missing.push("s3");
          if (missing.length) {
            sub.next({ type: "error", data: { error: "cfg_missing", missing } });
            this.logger.warn(`systemlogin auto cfg_missing user=${username || "-"} missing=${missing.join(",")}`);
            sub.complete();
            return;
          }

          const loginArgs: unknown[] = [username, passwordB64, ts, n1, n2, s1, s2, s3, v];
          sub.next({
            type: "request",
            data: {
              step: "loginUser",
              target: "SystemLogin.loginUser",
              args_preview: [username, "***", ts, n1, n2, s1, s2.slice(0, 16) + "…", s3.slice(0, 16) + "…", v],
            },
          });

          const loginPayload = this.amf.buildRemotingRequest({ target: "SystemLogin.loginUser", args: loginArgs });
          const loginUpstream = await this.amf.fetchUpstream(resolved.url, headers, loginPayload, abort.signal);
          const loginBuf = Buffer.from(await loginUpstream.arrayBuffer());
          const loginDecoded = this.amf.decodeRemotingEnvelope(loginBuf);
          const statusCodeRaw = extractByKey(loginDecoded, "status");
          const errorCodeRaw = extractByKey(loginDecoded, "error");
          const resultRaw =
            extractByKey(loginDecoded, "result") ??
            extractByKey(loginDecoded, "message") ??
            extractByKey(loginDecoded, "error_message") ??
            null;

          const statusCode = Number(statusCodeRaw);
          const errorCode = Number(errorCodeRaw);

          const sessionKeyRaw = this.amf.extractSessionKey(loginDecoded);
          const sessionKey = sessionKeyRaw ? String(sessionKeyRaw).trim() : "";
          const sessionKeyValid = /^[0-9a-f]{40}$/i.test(sessionKey);

          sub.next({
            type: "response",
            data: {
              step: "loginUser",
              http_status: loginUpstream.status,
              decoded: loginDecoded,
              extracted: {
                session_key: sessionKeyValid ? sessionKey : null,
                session_key_valid: sessionKeyValid,
                status: Number.isFinite(statusCode) ? statusCode : statusCodeRaw,
                error: Number.isFinite(errorCode) ? errorCode : errorCodeRaw,
              },
            },
          });

          if (!Number.isFinite(statusCode) || !Number.isFinite(errorCode)) {
            sub.next({ type: "error", data: { error: "login_response_invalid", reason: "status/error missing" } });
            this.logger.warn(`systemlogin auto login_response_invalid user=${username} upstream=${resolvedUrl}`);
            sub.complete();
            return;
          }

          if (statusCode !== 1 || errorCode !== 0) {
            sub.next({
              type: "error",
              data: { error: "login_failed", status: statusCode, code: errorCode, reason: resultRaw },
            });
            this.logger.warn(
              `systemlogin auto login_failed user=${username} upstream=${resolvedUrl} status=${statusCode} error=${errorCode}`
            );
            sub.complete();
            return;
          }

          if (!sessionKeyValid) {
            sub.next({ type: "error", data: { error: "session_key_invalid", reason: "bad_format" } });
            this.logger.warn(`systemlogin auto session_key_invalid user=${username} upstream=${resolvedUrl}`);
            sub.complete();
            return;
          }

          this.logger.log(`systemlogin auto login ok user=${username} session=${sessionKey.slice(0, 8)}…`);

          const userIdRaw =
            extractByKey(loginDecoded, "userid") ??
            extractByKey(loginDecoded, "user_id") ??
            extractByKey(loginDecoded, "uid") ??
            extractByKey(loginDecoded, "accountid") ??
            extractByKey(loginDecoded, "account_id") ??
            cfg.user_id ??
            cfg.userId ??
            "38984";

          const userId = String(userIdRaw ?? "").trim() || "38984";

          sub.next({
            type: "request",
            data: {
              step: "getAllCharacters",
              target: "SystemLogin.getAllCharacters",
              args_preview: [userId, sessionKey.slice(0, 12) + "…"],
            },
          });

          const allCharsPayload = this.amf.buildRemotingRequest({
            target: "SystemLogin.getAllCharacters",
            args: [userId, sessionKey],
          });
          const allCharsUpstream = await this.amf.fetchUpstream(resolved.url, headers, allCharsPayload, abort.signal);
          const allCharsBuf = Buffer.from(await allCharsUpstream.arrayBuffer());
          const allCharsDecoded = this.amf.decodeRemotingEnvelope(allCharsBuf);

          sub.next({
            type: "response",
            data: {
              step: "getAllCharacters",
              http_status: allCharsUpstream.status,
              decoded: allCharsDecoded,
            },
          });

          const cfgCharId = Number(cfg.char_id ?? cfg.charId);
          const charId =
            (Number.isFinite(cfgCharId) && cfgCharId > 0 ? cfgCharId : null) ??
            extractFirstCharId(allCharsDecoded) ??
            772;

          sub.next({
            type: "request",
            data: {
              step: "getCharacterData",
              target: "SystemLogin.getCharacterData",
              args_preview: [charId, sessionKey.slice(0, 12) + "…"],
            },
          });

          const charDataPayload = this.amf.buildRemotingRequest({
            target: "SystemLogin.getCharacterData",
            args: [charId, sessionKey],
          });
          const charDataUpstream = await this.amf.fetchUpstream(resolved.url, headers, charDataPayload, abort.signal);
          const charDataBuf = Buffer.from(await charDataUpstream.arrayBuffer());
          const charDataDecoded = this.amf.decodeRemotingEnvelope(charDataBuf);

          sub.next({
            type: "response",
            data: {
              step: "getCharacterData",
              http_status: charDataUpstream.status,
              decoded: charDataDecoded,
              extracted: { session_key: sessionKey, user_id: userId, char_id: charId },
            },
          });

          sub.next({
            type: "done",
            data: { session_key: sessionKey, user_id: userId, char_id: charId },
          });
          sub.complete();
        } catch (err: any) {
          sub.next({
            type: "error",
            data: { error: "auto_connect_failed", message: String(err?.message || err), url: resolvedUrl },
          });
          this.logger.error(`systemlogin auto exception url=${resolvedUrl} msg=${String(err?.message || err)}`);
          sub.complete();
        }
      })();

      return () => {
        abort.abort();
        req.off?.("close", onClose);
        req.off?.("aborted", onClose);
      };
    });
  }

}
