import { createHash, randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";

import { fileExists } from "./http-utils.mjs";

const defaultAuthFilePath = path.join(os.homedir(), ".codex", "nbtanginone-auth.json");
const pendingRequestTtlMs = 10 * 60 * 1000;
const approvedClientTtlMs = 7 * 24 * 60 * 60 * 1000;
const approvedClientTtlDays = 7;
const bootstrapApprovedClientTtlDays = 365;

function daysToMs(days) {
  return days * 24 * 60 * 60 * 1000;
}

function normalizeApprovedDays(value) {
  const days = Number(value);
  return Number.isFinite(days) && days > 0 ? Math.trunc(days) : approvedClientTtlDays;
}

function normalizeRequest(item) {
  if (!item || typeof item !== "object" || typeof item.id !== "string") return null;
  return {
    id: item.id,
    createdAt: typeof item.createdAt === "string" ? item.createdAt : "",
    status: typeof item.status === "string" ? item.status : "pending",
    fingerprint: typeof item.fingerprint === "string" ? item.fingerprint : "",
    clientId: typeof item.clientId === "string" ? item.clientId : "",
    clientIp: typeof item.clientIp === "string" ? item.clientIp : "",
    publicIp: typeof item.publicIp === "string" ? item.publicIp : "",
    publicLocation: typeof item.publicLocation === "string" ? item.publicLocation : "",
    userAgent: typeof item.userAgent === "string" ? item.userAgent : "",
    acceptLanguage: typeof item.acceptLanguage === "string" ? item.acceptLanguage : "",
    secChUa: typeof item.secChUa === "string" ? item.secChUa : "",
    secChUaPlatform: typeof item.secChUaPlatform === "string" ? item.secChUaPlatform : "",
    screen: typeof item.screen === "string" ? item.screen : "",
    timezone: typeof item.timezone === "string" ? item.timezone : "",
    platform: typeof item.platform === "string" ? item.platform : "",
    touchPoints: Number.isFinite(item.touchPoints) ? item.touchPoints : 0,
    cookieId: typeof item.cookieId === "string" ? item.cookieId : "",
    note: typeof item.note === "string" ? item.note : "",
    approvedDays: normalizeApprovedDays(item.approvedDays),
    decisionAt: typeof item.decisionAt === "string" ? item.decisionAt : "",
    decisionBy: typeof item.decisionBy === "string" ? item.decisionBy : "",
  };
}

function normalizeApprovedClient(item) {
  if (!item || typeof item !== "object" || typeof item.clientId !== "string") return null;
  return {
    clientId: item.clientId,
    fingerprint: typeof item.fingerprint === "string" ? item.fingerprint : "",
    approvedAt: typeof item.approvedAt === "string" ? item.approvedAt : "",
    expiresAt: typeof item.expiresAt === "string" ? item.expiresAt : "",
    label: typeof item.label === "string" ? item.label : "",
    sessionToken: typeof item.sessionToken === "string" ? item.sessionToken : "",
    requestId: typeof item.requestId === "string" ? item.requestId : "",
    lastSeenAt: typeof item.lastSeenAt === "string" ? item.lastSeenAt : "",
    isBootstrapApprover: item.isBootstrapApprover === true,
  };
}

function ensureBootstrapApprover(list) {
  if (!Array.isArray(list) || list.length === 0) return [];
  const normalized = list.map((item) => normalizeApprovedClient(item)).filter(Boolean);
  if (normalized.some((item) => item.isBootstrapApprover)) {
    return normalized;
  }

  let selectedIndex = 0;
  let selectedTime = Number.POSITIVE_INFINITY;
  for (let index = 0; index < normalized.length; index += 1) {
    const value = Date.parse(normalized[index].approvedAt || 0);
    if (Number.isFinite(value) && value < selectedTime) {
      selectedTime = value;
      selectedIndex = index;
    }
  }
  normalized[selectedIndex].isBootstrapApprover = true;
  return normalized;
}

function dedupeBy(list, key) {
  return Array.from(new Map(list.map((item) => [item[key], item])).values());
}

function isNotExpired(isoString, now) {
  if (!isoString) return false;
  const value = Date.parse(isoString);
  return Number.isFinite(value) && value > now;
}

export class AuthStore {
  constructor(options = {}) {
    this.filePath = options.filePath || process.env.NBTANGINONE_AUTH_FILE || defaultAuthFilePath;
  }

  async loadState() {
    if (!(await fileExists(fs, this.filePath))) {
      return { pendingRequests: [], approvedClients: [] };
    }

    try {
      const parsed = JSON.parse(await fs.readFile(this.filePath, "utf8"));
      return {
        pendingRequests: Array.isArray(parsed.pendingRequests)
          ? parsed.pendingRequests.map(normalizeRequest).filter(Boolean)
          : [],
        approvedClients: Array.isArray(parsed.approvedClients)
          ? parsed.approvedClients.map(normalizeApprovedClient).filter(Boolean)
          : [],
      };
    } catch {
      return { pendingRequests: [], approvedClients: [] };
    }
  }

  async saveState(state) {
    const approvedClients = ensureBootstrapApprover(
      Array.isArray(state.approvedClients) ? state.approvedClients.map(normalizeApprovedClient).filter(Boolean) : []
    );
    const content =
      JSON.stringify(
        {
          pendingRequests: dedupeBy(
            (Array.isArray(state.pendingRequests) ? state.pendingRequests.map(normalizeRequest).filter(Boolean) : []),
            "id"
          ).sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))),
          approvedClients: dedupeBy(approvedClients, "clientId").sort((a, b) =>
            String(b.approvedAt).localeCompare(String(a.approvedAt))
          ),
        },
        null,
        2
      ) + "\n";
    const tmpPath = this.filePath + ".tmp." + randomUUID();
    await fs.writeFile(tmpPath, content, "utf8");
    await fs.rename(tmpPath, this.filePath);
  }

  async cleanupState() {
    const state = await this.loadState();
    const now = Date.now();
    const cleanedState = {
      pendingRequests: state.pendingRequests.filter((item) => {
        if (item.status === "pending") {
          return isNotExpired(new Date(Date.parse(item.createdAt || 0) + pendingRequestTtlMs).toISOString(), now);
        }
        return isNotExpired(
          new Date(Date.parse(item.createdAt || 0) + daysToMs(normalizeApprovedDays(item.approvedDays))).toISOString(),
          now
        );
      }),
      approvedClients: state.approvedClients.filter((item) => isNotExpired(item.expiresAt, now)),
    };
    cleanedState.approvedClients = ensureBootstrapApprover(cleanedState.approvedClients);
    await this.saveState(cleanedState);
    return cleanedState;
  }

  getClientIp(req) {
    const forwardedFor = req.headers["x-forwarded-for"];
    if (typeof forwardedFor === "string" && forwardedFor.trim()) {
      return forwardedFor.split(",")[0].trim();
    }
    return req.socket?.remoteAddress || "";
  }

  createFingerprintHash(payload) {
    return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
  }

  async createOrReuseRequest(req, payload) {
    const state = await this.cleanupState();
    const nowIso = new Date().toISOString();
    const nowMs = Date.now();
    const requestPayload = {
      clientId: typeof payload.clientId === "string" ? payload.clientId : "",
      fingerprint: typeof payload.fingerprint === "string" ? payload.fingerprint : "",
      userAgent: typeof payload.userAgent === "string" ? payload.userAgent : "",
      acceptLanguage: typeof payload.acceptLanguage === "string" ? payload.acceptLanguage : "",
      secChUa: typeof payload.secChUa === "string" ? payload.secChUa : "",
      secChUaPlatform: typeof payload.secChUaPlatform === "string" ? payload.secChUaPlatform : "",
      screen: typeof payload.screen === "string" ? payload.screen : "",
      timezone: typeof payload.timezone === "string" ? payload.timezone : "",
      platform: typeof payload.platform === "string" ? payload.platform : "",
      touchPoints: Number.isFinite(payload.touchPoints) ? payload.touchPoints : 0,
      cookieId: typeof payload.cookieId === "string" ? payload.cookieId : "",
      clientIp: this.getClientIp(req),
      publicIp: typeof payload.publicIp === "string" ? payload.publicIp : "",
      publicLocation: typeof payload.publicLocation === "string" ? payload.publicLocation : "",
    };

    if (state.pendingRequests.length === 0 && state.approvedClients.length === 0) {
      const approvedClient = {
        clientId: requestPayload.clientId,
        fingerprint: requestPayload.fingerprint,
        approvedAt: nowIso,
        expiresAt: new Date(Date.now() + daysToMs(bootstrapApprovedClientTtlDays)).toISOString(),
        label: `${requestPayload.platform || "browser"} ${requestPayload.screen || ""}`.trim(),
        sessionToken: randomUUID(),
        requestId: "",
        lastSeenAt: nowIso,
        isBootstrapApprover: true,
      };
      state.approvedClients.unshift(approvedClient);
      await this.saveState(state);
      return {
        request: null,
        status: "approved",
        approvedClient,
      };
    }

    const approvedClient = state.approvedClients.find(
      (item) =>
        item.clientId === requestPayload.clientId &&
        isNotExpired(item.expiresAt, nowMs)
    );

    if (approvedClient) {
      approvedClient.fingerprint = requestPayload.fingerprint;
      approvedClient.lastSeenAt = nowIso;
      approvedClient.expiresAt = new Date(Date.now() + daysToMs(normalizeApprovedDays(approvedClientTtlDays))).toISOString();
      if (!approvedClient.sessionToken) {
        approvedClient.sessionToken = randomUUID();
      }
      await this.saveState(state);
      return {
        request: null,
        status: "approved",
        approvedClient,
      };
    }

    const existingPending = state.pendingRequests.find(
      (item) =>
        item.status === "pending" &&
        item.clientId === requestPayload.clientId &&
        nowMs - Date.parse(item.createdAt || 0) < pendingRequestTtlMs
    );

    if (existingPending) {
      if (requestPayload.publicIp && !existingPending.publicIp) {
        existingPending.publicIp = requestPayload.publicIp;
      }
      if (requestPayload.publicLocation && !existingPending.publicLocation) {
        existingPending.publicLocation = requestPayload.publicLocation;
      }
      if (requestPayload.clientIp && !existingPending.clientIp) {
        existingPending.clientIp = requestPayload.clientIp;
      }
      await this.saveState(state);
      return {
        request: existingPending,
        status: existingPending.status,
        approvedClient: null,
      };
    }

    const request = {
      id: "req_" + randomUUID(),
      createdAt: nowIso,
      status: "pending",
      ...requestPayload,
      note: "",
      approvedDays: approvedClientTtlDays,
      decisionAt: "",
      decisionBy: "",
    };
    state.pendingRequests.unshift(request);
    await this.saveState(state);
    return {
      request,
      status: request.status,
      approvedClient: null,
    };
  }

  async getRequestStatus(requestId) {
    const state = await this.cleanupState();
    const request = state.pendingRequests.find((item) => item.id === requestId);
    if (!request) return { request: null, approvedClient: null };

    let approvedClient = null;
    if (request.status === "approved") {
      const nowIso = new Date().toISOString();
      const approvedDays = normalizeApprovedDays(request.approvedDays);
      const expiresAt = new Date(Date.now() + daysToMs(approvedDays)).toISOString();
      approvedClient = state.approvedClients.find((item) => item.clientId === request.clientId);
      if (!approvedClient) {
        approvedClient = {
          clientId: request.clientId,
          fingerprint: request.fingerprint,
          approvedAt: request.decisionAt || nowIso,
          expiresAt,
          label: `${request.platform || "browser"} ${request.screen || ""}`.trim(),
          sessionToken: randomUUID(),
          requestId: request.id,
          lastSeenAt: nowIso,
          isBootstrapApprover: false,
        };
        state.approvedClients.unshift(approvedClient);
      } else {
        approvedClient.fingerprint = request.fingerprint;
        approvedClient.requestId = request.id;
        approvedClient.lastSeenAt = nowIso;
        approvedClient.expiresAt = expiresAt;
        if (!approvedClient.sessionToken) {
          approvedClient.sessionToken = randomUUID();
        }
      }
      await this.saveState(state);
    }

    return { request, approvedClient };
  }

  async verifySessionToken(token) {
    if (!token) return null;
    const state = await this.cleanupState();
    const nowMs = Date.now();
    const approvedClient = state.approvedClients.find(
      (item) => item.sessionToken === token && isNotExpired(item.expiresAt, nowMs)
    );
    if (!approvedClient) return null;
    approvedClient.lastSeenAt = new Date().toISOString();
    approvedClient.expiresAt = new Date(Date.now() + daysToMs(normalizeApprovedDays(approvedClientTtlDays))).toISOString();
    await this.saveState(state);
    return approvedClient;
  }

  async listPendingRequests() {
    const state = await this.cleanupState();
    return state.pendingRequests.filter((item) => item.status === "pending");
  }

  async listApprovedClients() {
    const state = await this.cleanupState();
    return state.approvedClients;
  }

  async decideRequest(requestId, decision, decidedBy = "local-script", approvedDays = approvedClientTtlDays) {
    const state = await this.cleanupState();
    const request = state.pendingRequests.find((item) => item.id === requestId);
    if (!request) {
      const error = new Error("Request not found");
      error.statusCode = 404;
      throw error;
    }
    if (!["approved", "rejected"].includes(decision)) {
      const error = new Error("Invalid decision");
      error.statusCode = 400;
      throw error;
    }
    request.status = decision;
    request.approvedDays = normalizeApprovedDays(approvedDays);
    request.decisionAt = new Date().toISOString();
    request.decisionBy = decidedBy;
    await this.saveState(state);
    return request;
  }

  async revokeApprovedClient(clientId) {
    const state = await this.cleanupState();
    const approvedClient = state.approvedClients.find((item) => item.clientId === clientId);
    if (!approvedClient) {
      const error = new Error("Approved client not found");
      error.statusCode = 404;
      throw error;
    }
    if (approvedClient.isBootstrapApprover) {
      const error = new Error("Bootstrap approver cannot be revoked");
      error.statusCode = 400;
      throw error;
    }

    state.approvedClients = state.approvedClients.filter((item) => item.clientId !== clientId);
    await this.saveState(state);
    return { clientId };
  }
}