import type { AppConfig, CaptchaResult, CloudResult, LoginRequest, SessionStatus } from "../../shared/types";
import { SessionState, type PersistedSession } from "./session";

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

interface SessionPersistence {
  save(session: PersistedSession): Promise<void>;
  clear(): Promise<void>;
}

export class SmqjhClient {
  constructor(
    private readonly getConfig: () => AppConfig,
    private readonly session: SessionState,
    private readonly persistence?: SessionPersistence
  ) {}

  async login(request: LoginRequest): Promise<CloudResult> {
    const config = this.getConfig();
    const body = new FormData();
    body.append("grant_type", config.auth.grantType);
    body.append("username", request.username);
    body.append("password", request.password);
    const tenantCode = tenantCodeForUser(request.username, request.tenantCode);
    if (tenantCode) {
      body.append("tenantCode", tenantCode);
    }
    body.append("captchaId", request.captchaId);
    body.append("captchaCode", request.captchaCode);

    const authHeader = Buffer.from(`${config.auth.clientId}:${config.auth.clientSecret}`).toString("base64");
    const headers: Record<string, string> = {
      Authorization: `Basic ${authHeader}`
    };
    if (config.tenantDomain.trim()) {
      headers["X-Tenant-Domain"] = config.tenantDomain.trim();
    }
    if (tenantCode) {
      headers["X-Tenant-Code"] = tenantCode;
    }

    const response = await this.fetchRaw(config.auth.tokenPath, {
      method: "POST",
      headers,
      body
    });

    const normalized = await this.normalizeResponse(response);
    const tokenInfo = this.pickTokenInfo(normalized.data ?? normalized.raw);
    if (normalized.ok && tokenInfo.accessToken) {
      this.session.set({
        accessToken: tokenInfo.accessToken,
        refreshToken: tokenInfo.refreshToken,
        expiresIn: tokenInfo.expiresIn,
        username: request.username
      });
      await this.persistSession();
    }

    return normalized;
  }

  async logout(): Promise<SessionStatus> {
    const status = this.session.clear();
    await this.persistence?.clear();
    return status;
  }

  async ensureSessionFresh(): Promise<SessionStatus> {
    if (!this.session.isExpired(60_000)) {
      return this.session.status();
    }

    const refreshToken = this.session.refresh();
    if (!refreshToken) {
      return this.logout();
    }

    const result = await this.refreshAccessToken(refreshToken).catch((error) => ({
      ok: false,
      status: 0,
      message: error instanceof Error ? error.message : String(error)
    }));

    if (!result.ok && (result.status === 400 || result.status === 401)) {
      return this.logout();
    }

    return this.session.status();
  }

  async getCaptcha(): Promise<CloudResult<CaptchaResult>> {
    return this.request<CaptchaResult>("GET", "/smqjh-auth/api/v1/auth/captcha", { auth: false });
  }

  async request<T>(
    method: HttpMethod,
    endpoint: string,
    options: {
      query?: Record<string, string>;
      body?: unknown;
      auth?: boolean;
    } = {}
  ): Promise<CloudResult<T>> {
    const headers: Record<string, string> = {
      Accept: "application/json",
      ...this.contextHeaders()
    };

    if (options.auth !== false) {
      await this.ensureSessionFresh();
      const token = this.session.token();
      if (!token) {
        return {
          ok: false,
          status: 401,
          message: "当前未登录，请先完成后台账号登录"
        };
      }
      headers.Authorization = `Bearer ${token}`;
    }

    let body: BodyInit | undefined;
    if (options.body !== undefined) {
      headers["Content-Type"] = "application/json";
      body = JSON.stringify(options.body);
    }

    const response = await this.fetchRaw(this.withQuery(endpoint, options.query), {
      method,
      headers,
      body
    });

    return this.normalizeResponse<T>(response);
  }

  async ping(): Promise<CloudResult> {
    return this.getCaptcha();
  }

  private async refreshAccessToken(refreshToken: string): Promise<CloudResult> {
    const config = this.getConfig();
    const current = this.session.snapshot();
    const body = new FormData();
    body.append("grant_type", "refresh_token");
    body.append("refresh_token", refreshToken);
    const tenantCode = tenantCodeForUser(current?.username, config.tenantCode);
    if (tenantCode) {
      body.append("tenantCode", tenantCode);
    }

    const headers: Record<string, string> = {
      Authorization: this.basicAuthHeader()
    };
    if (config.tenantDomain.trim()) {
      headers["X-Tenant-Domain"] = config.tenantDomain.trim();
    }
    if (tenantCode) {
      headers["X-Tenant-Code"] = tenantCode;
    }

    const response = await this.fetchRaw(config.auth.tokenPath, {
      method: "POST",
      headers,
      body
    });

    const normalized = await this.normalizeResponse(response);
    const tokenInfo = this.pickTokenInfo(normalized.data ?? normalized.raw);
    if (normalized.ok && tokenInfo.accessToken) {
      this.session.set({
        accessToken: tokenInfo.accessToken,
        refreshToken: tokenInfo.refreshToken ?? refreshToken,
        expiresIn: tokenInfo.expiresIn,
        username: current?.username
      });
      await this.persistSession();
    }

    return normalized;
  }

  private async persistSession(): Promise<void> {
    const snapshot = this.session.snapshot();
    if (snapshot) {
      await this.persistence?.save(snapshot);
    }
  }

  private async fetchRaw(endpoint: string, init: RequestInit): Promise<Response> {
    const config = this.getConfig();
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), config.requestTimeoutMs);

    try {
      return await fetch(this.absoluteUrl(endpoint), {
        ...init,
        signal: controller.signal
      });
    } finally {
      clearTimeout(timeout);
    }
  }

  private absoluteUrl(endpoint: string): string {
    const config = this.getConfig();
    return new URL(endpoint, config.baseUrl).toString();
  }

  private withQuery(endpoint: string, query?: Record<string, string>): string {
    if (!query || Object.keys(query).length === 0) {
      return endpoint;
    }

    const url = new URL(endpoint, "http://agent.local");
    Object.entries(query).forEach(([key, value]) => {
      if (value !== "") {
        url.searchParams.set(key, value);
      }
    });

    return `${url.pathname}${url.search}`;
  }

  private basicAuthHeader(): string {
    const config = this.getConfig();
    return `Basic ${Buffer.from(`${config.auth.clientId}:${config.auth.clientSecret}`).toString("base64")}`;
  }

  private contextHeaders(): Record<string, string> {
    const config = this.getConfig();
    const username = this.session.status().username;
    const headers: Record<string, string> = {};
    if (username) {
      headers["X-Operator-Name"] = username;
    }
    const tenantCode = tenantCodeForUser(username, config.tenantCode);
    if (tenantCode) {
      headers["X-Tenant-Code"] = tenantCode;
    }
    if (config.tenantDomain.trim()) {
      headers["X-Tenant-Domain"] = config.tenantDomain.trim();
    }
    return headers;
  }

  private async normalizeResponse<T>(response: Response): Promise<CloudResult<T>> {
    const contentType = response.headers.get("content-type") ?? "";
    const raw = contentType.includes("application/json")
      ? await response.json().catch(() => undefined)
      : await response.text().catch(() => undefined);

    const data = this.unwrapData<T>(raw);
    const code = typeof raw === "object" && raw !== null && "code" in raw ? String((raw as { code: unknown }).code) : "";
    const message = this.pickMessage(raw) ?? response.statusText;
    const okByBusinessCode = code === "" || code === "0" || code === "200" || code === "00000";

    return {
      ok: response.ok && okByBusinessCode,
      status: response.status,
      message,
      data,
      raw
    };
  }

  private unwrapData<T>(raw: unknown): T | undefined {
    if (typeof raw === "object" && raw !== null && "data" in raw) {
      return (raw as { data?: T }).data;
    }
    return raw as T | undefined;
  }

  private pickMessage(raw: unknown): string | undefined {
    if (typeof raw !== "object" || raw === null) {
      return undefined;
    }

    const value = raw as Record<string, unknown>;
    return String(value.msg ?? value.message ?? value.error_description ?? value.error ?? "") || undefined;
  }

  private pickTokenInfo(raw: unknown): { accessToken?: string; refreshToken?: string; expiresIn?: number } {
    const token = this.unwrapData<Record<string, unknown>>(raw) ?? (raw as Record<string, unknown> | undefined);
    if (!token || typeof token !== "object") {
      return {};
    }

    return {
      accessToken: String(token.access_token ?? token.accessToken ?? ""),
      refreshToken: token.refresh_token || token.refreshToken ? String(token.refresh_token ?? token.refreshToken) : undefined,
      expiresIn: Number(token.expires_in ?? token.expiresIn ?? 0) || undefined
    };
  }
}

function tenantCodeForUser(username: string | undefined, tenantCode: string | undefined): string {
  return (tenantCode ?? "").trim();
}
