import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import type { FastifyReply, FastifyRequest } from "fastify";
import { createId } from "./id.js";
import { prisma } from "./prisma.js";

const WORKSPACE_API_TOKEN_PREFIX = "nva";
const WORKSPACE_API_SECRET_BYTES = 24;

type WorkspaceApiTokenRow = {
  id: string;
  law_firm_id: string;
  name: string;
  token_prefix: string;
  token_last4: string;
  secret_hash: string;
  created_by_user_id: string | null;
  last_used_at: Date | null;
  revoked_at: Date | null;
  created_at: Date;
  updated_at: Date;
};

type ActiveWorkspaceApiTokenRow = WorkspaceApiTokenRow & {
  workspace_name: string;
  workspace_slug: string;
  workspace_status: string;
};

export type WorkspaceApiTokenSummary = {
  id: string;
  lawFirmId: string;
  name: string;
  tokenPrefix: string;
  tokenLast4: string;
  createdByUserId: string | null;
  lastUsedAt: Date | null;
  revokedAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
};

export type WorkspaceApiAccessContext = {
  tokenId: string;
  tokenName: string;
  lawFirm: {
    id: string;
    name: string;
    slug: string;
    status: string;
  };
};

let ensureWorkspaceApiTokenSchemaPromise: Promise<void> | null = null;

function hashWorkspaceApiSecret(secret: string) {
  return createHash("sha256").update(secret).digest("hex");
}

function buildWorkspaceApiPlainToken(tokenId: string, secret: string) {
  return `${WORKSPACE_API_TOKEN_PREFIX}_${tokenId}.${secret}`;
}

function parseWorkspaceApiPlainToken(token: string) {
  const normalized = String(token ?? "").trim();
  const prefix = `${WORKSPACE_API_TOKEN_PREFIX}_`;

  if (!normalized.startsWith(prefix)) {
    return null;
  }

  const separatorIndex = normalized.indexOf(".");

  if (separatorIndex < 0) {
    return null;
  }

  const tokenId = normalized.slice(prefix.length, separatorIndex).trim();
  const secret = normalized.slice(separatorIndex + 1).trim();

  if (!tokenId || !secret) {
    return null;
  }

  return {
    tokenId,
    secret,
  };
}

function normalizeBearerToken(
  authorizationHeader: string | string[] | undefined,
) {
  const rawValue =
    typeof authorizationHeader === "string" ?
      authorizationHeader :
      Array.isArray(authorizationHeader) ?
        authorizationHeader[0] :
        "";
  const trimmedValue = rawValue.trim();

  if (!trimmedValue.toLowerCase().startsWith("bearer ")) {
    return "";
  }

  return trimmedValue.slice("bearer ".length).trim();
}

function createTokenPrefix(tokenId: string) {
  return `${WORKSPACE_API_TOKEN_PREFIX}_${tokenId.slice(0, 8)}`;
}

function serializeWorkspaceApiToken(row: WorkspaceApiTokenRow): WorkspaceApiTokenSummary {
  return {
    id: row.id,
    lawFirmId: row.law_firm_id,
    name: row.name,
    tokenPrefix: row.token_prefix,
    tokenLast4: row.token_last4,
    createdByUserId: row.created_by_user_id,
    lastUsedAt: row.last_used_at,
    revokedAt: row.revoked_at,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
  };
}

export async function ensureWorkspaceApiTokenSchema() {
  if (!ensureWorkspaceApiTokenSchemaPromise) {
    ensureWorkspaceApiTokenSchemaPromise = (async () => {
      await prisma.$executeRawUnsafe(`
        CREATE TABLE IF NOT EXISTS workspace_api_tokens (
          id CHAR(36) NOT NULL COMMENT 'Application-generated UUIDv7',
          law_firm_id CHAR(36) NOT NULL,
          name VARCHAR(120) NOT NULL,
          token_prefix VARCHAR(32) NOT NULL,
          token_last4 CHAR(4) NOT NULL,
          secret_hash CHAR(64) NOT NULL,
          created_by_user_id CHAR(36) NULL,
          last_used_at DATETIME NULL,
          revoked_at DATETIME NULL,
          created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
          PRIMARY KEY (id),
          UNIQUE KEY uq_workspace_api_tokens_secret_hash (secret_hash),
          KEY idx_workspace_api_tokens_law_firm (law_firm_id, revoked_at, created_at),
          KEY idx_workspace_api_tokens_created_by (created_by_user_id),
          CONSTRAINT fk_workspace_api_tokens_law_firm
            FOREIGN KEY (law_firm_id) REFERENCES law_firms (id),
          CONSTRAINT fk_workspace_api_tokens_created_by
            FOREIGN KEY (created_by_user_id) REFERENCES users (id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
      `);
    })().catch((error) => {
      ensureWorkspaceApiTokenSchemaPromise = null;
      throw error;
    });
  }

  await ensureWorkspaceApiTokenSchemaPromise;
}

export async function listWorkspaceApiTokens(lawFirmId: string) {
  await ensureWorkspaceApiTokenSchema();

  const rows = await prisma.$queryRaw<WorkspaceApiTokenRow[]>`
    SELECT
      id,
      law_firm_id,
      name,
      token_prefix,
      token_last4,
      secret_hash,
      created_by_user_id,
      last_used_at,
      revoked_at,
      created_at,
      updated_at
    FROM workspace_api_tokens
    WHERE law_firm_id = ${lawFirmId}
    ORDER BY revoked_at IS NULL DESC, created_at DESC
  `;

  return rows.map((row) => serializeWorkspaceApiToken(row));
}

export async function createWorkspaceApiToken(input: {
  lawFirmId: string;
  name: string;
  createdByUserId: string | null;
}) {
  await ensureWorkspaceApiTokenSchema();

  const tokenId = createId();
  const secret = randomBytes(WORKSPACE_API_SECRET_BYTES).toString("hex");
  const plainTextToken = buildWorkspaceApiPlainToken(tokenId, secret);
  const tokenPrefix = createTokenPrefix(tokenId);
  const tokenLast4 = secret.slice(-4);

  await prisma.$executeRaw`
    INSERT INTO workspace_api_tokens (
      id,
      law_firm_id,
      name,
      token_prefix,
      token_last4,
      secret_hash,
      created_by_user_id,
      created_at,
      updated_at
    ) VALUES (
      ${tokenId},
      ${input.lawFirmId},
      ${input.name.trim()},
      ${tokenPrefix},
      ${tokenLast4},
      ${hashWorkspaceApiSecret(secret)},
      ${input.createdByUserId},
      CURRENT_TIMESTAMP,
      CURRENT_TIMESTAMP
    )
  `;

  const [createdToken] = await prisma.$queryRaw<WorkspaceApiTokenRow[]>`
    SELECT
      id,
      law_firm_id,
      name,
      token_prefix,
      token_last4,
      secret_hash,
      created_by_user_id,
      last_used_at,
      revoked_at,
      created_at,
      updated_at
    FROM workspace_api_tokens
    WHERE id = ${tokenId}
    LIMIT 1
  `;

  if (!createdToken) {
    throw new Error("Failed to load created workspace API token");
  }

  return {
    token: serializeWorkspaceApiToken(createdToken),
    plainTextToken,
  };
}

export async function revokeWorkspaceApiToken(input: {
  lawFirmId: string;
  tokenId: string;
}) {
  await ensureWorkspaceApiTokenSchema();

  await prisma.$executeRaw`
    UPDATE workspace_api_tokens
    SET
      revoked_at = CURRENT_TIMESTAMP,
      updated_at = CURRENT_TIMESTAMP
    WHERE id = ${input.tokenId}
      AND law_firm_id = ${input.lawFirmId}
      AND revoked_at IS NULL
  `;
}

export async function requireWorkspaceApiAccess(
  request: FastifyRequest,
  reply: FastifyReply,
) {
  await ensureWorkspaceApiTokenSchema();

  const rawBearerToken = normalizeBearerToken(request.headers.authorization);

  if (!rawBearerToken) {
    throw reply.unauthorized("Bearer token required");
  }

  const parsedToken = parseWorkspaceApiPlainToken(rawBearerToken);

  if (!parsedToken) {
    throw reply.unauthorized("Invalid workspace API token");
  }

  const [tokenRow] = await prisma.$queryRaw<ActiveWorkspaceApiTokenRow[]>`
    SELECT
      wat.id,
      wat.law_firm_id,
      wat.name,
      wat.token_prefix,
      wat.token_last4,
      wat.secret_hash,
      wat.created_by_user_id,
      wat.last_used_at,
      wat.revoked_at,
      wat.created_at,
      wat.updated_at,
      lf.name AS workspace_name,
      lf.slug AS workspace_slug,
      lf.status AS workspace_status
    FROM workspace_api_tokens wat
    INNER JOIN law_firms lf ON lf.id = wat.law_firm_id
    WHERE wat.id = ${parsedToken.tokenId}
      AND wat.revoked_at IS NULL
      AND lf.deleted_at IS NULL
    LIMIT 1
  `;

  if (!tokenRow || tokenRow.workspace_status !== "active") {
    throw reply.unauthorized("Invalid workspace API token");
  }

  const providedHash = Buffer.from(hashWorkspaceApiSecret(parsedToken.secret), "hex");
  const storedHash = Buffer.from(tokenRow.secret_hash, "hex");

  if (
    providedHash.length !== storedHash.length ||
    !timingSafeEqual(providedHash, storedHash)
  ) {
    throw reply.unauthorized("Invalid workspace API token");
  }

  await prisma.$executeRaw`
    UPDATE workspace_api_tokens
    SET last_used_at = CURRENT_TIMESTAMP,
        updated_at = CURRENT_TIMESTAMP
    WHERE id = ${tokenRow.id}
  `;

  return {
    tokenId: tokenRow.id,
    tokenName: tokenRow.name,
    lawFirm: {
      id: tokenRow.law_firm_id,
      name: tokenRow.workspace_name,
      slug: tokenRow.workspace_slug,
      status: tokenRow.workspace_status,
    },
  } satisfies WorkspaceApiAccessContext;
}
