const TOKEN_PREFIX = 'kvault_';
const TOKEN_KEY_PREFIX = 'api_token:';
const VALID_SCOPES = new Set(['upload', 'read', 'delete', 'paste']);
const TOKEN_ID_LENGTH = 12;
const TOKEN_SECRET_LENGTH = 40;
const TOKEN_SALT_LENGTH = 16;
const MASK_PREFIX = '******';

function timingSafeEqual(a, b) {
  if (a.length !== b.length) return false;
  let mismatch = 0;
  for (let i = 0; i < a.length; i += 1) {
    mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return mismatch === 0;
}

function sanitizeTokenId(rawValue = '') {
  const value = String(rawValue || '').trim();
  if (!/^[A-Za-z0-9_-]{6,128}$/.test(value)) return '';
  return value;
}

function randomString(length) {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  const bytes = new Uint8Array(length);
  crypto.getRandomValues(bytes);
  let output = '';
  for (let i = 0; i < length; i += 1) {
    output += chars[bytes[i] % chars.length];
  }
  return output;
}

function splitToken(rawToken = '') {
  const value = String(rawToken || '').trim();
  const match = /^kvault_([A-Za-z0-9_-]{6,128})_([A-Za-z0-9_-]{16,256})$/.exec(value);
  if (!match) return null;
  return {
    tokenId: match[1],
    secret: match[2],
    value,
  };
}

function normalizeExpiresAt(rawValue) {
  if (rawValue == null || rawValue === '') return null;
  if (typeof rawValue === 'number') {
    return Number.isFinite(rawValue) ? Math.max(0, Math.floor(rawValue)) : null;
  }
  const text = String(rawValue).trim();
  if (!text) return null;
  const numeric = Number(text);
  if (Number.isFinite(numeric)) {
    return Math.max(0, Math.floor(numeric));
  }
  const parsed = Date.parse(text);
  if (Number.isFinite(parsed)) {
    return Math.max(0, Math.floor(parsed));
  }
  return null;
}

function normalizeScopes(rawScopes = []) {
  const list = Array.isArray(rawScopes) ? rawScopes : [rawScopes];
  const normalized = [];
  list.forEach((item) => {
    const scope = String(item || '').trim().toLowerCase();
    if (!VALID_SCOPES.has(scope)) return;
    if (normalized.includes(scope)) return;
    normalized.push(scope);
  });
  return normalized;
}

async function sha256Hex(input) {
  const bytes = new TextEncoder().encode(String(input || ''));
  const digest = await crypto.subtle.digest('SHA-256', bytes);
  return Array.from(new Uint8Array(digest))
    .map((byte) => byte.toString(16).padStart(2, '0'))
    .join('');
}

async function hashTokenSecret(secret, salt) {
  return sha256Hex(`${salt}:${secret}`);
}

function maskTokenSuffix(suffix = '') {
  const normalized = String(suffix || '');
  return `${MASK_PREFIX}${normalized}`;
}

function ensureKvBinding(env) {
  if (!env?.img_url) {
    throw new Error('KV binding img_url is not configured.');
  }
}

function toPublicRecord(record = {}) {
  return {
    id: record.id,
    name: record.name,
    scopes: Array.isArray(record.scopes) ? [...record.scopes] : [],
    expiresAt: record.expiresAt ?? null,
    createdAt: Number(record.createdAt || 0),
    lastUsedAt: record.lastUsedAt ?? null,
    enabled: Boolean(record.enabled),
    tokenPreview: record.tokenPreview || maskTokenSuffix(record.tokenSuffix || ''),
  };
}

async function getRecordById(tokenId, env) {
  ensureKvBinding(env);
  const id = sanitizeTokenId(tokenId);
  if (!id) return null;
  const value = await env.img_url.get(`${TOKEN_KEY_PREFIX}${id}`, { type: 'json' });
  if (!value || typeof value !== 'object') return null;
  return {
    ...value,
    id,
    scopes: normalizeScopes(value.scopes || []),
    enabled: value.enabled !== false,
    expiresAt: normalizeExpiresAt(value.expiresAt),
    createdAt: Number(value.createdAt || 0),
    lastUsedAt: value.lastUsedAt == null ? null : Number(value.lastUsedAt || 0),
  };
}

async function putRecord(record, env) {
  ensureKvBinding(env);
  await env.img_url.put(`${TOKEN_KEY_PREFIX}${record.id}`, JSON.stringify(record));
}

async function generateUniqueTokenId(env, maxAttempts = 10) {
  for (let i = 0; i < maxAttempts; i += 1) {
    const candidate = randomString(TOKEN_ID_LENGTH);
    const exists = await env.img_url.get(`${TOKEN_KEY_PREFIX}${candidate}`);
    if (exists == null) {
      return candidate;
    }
  }
  throw new Error('Failed to generate a unique token id.');
}

export function getApiTokenScopes() {
  return [...VALID_SCOPES];
}

export function parseBearerToken(request) {
  const authorization = String(request.headers.get('Authorization') || '').trim();
  if (!authorization) return '';
  const match = /^Bearer\s+(.+)$/i.exec(authorization);
  if (!match) return '';
  return String(match[1] || '').trim();
}

export async function createApiToken({ name, scopes, expiresAt, enabled = true }, env) {
  ensureKvBinding(env);

  const normalizedName = String(name || '').trim();
  if (!normalizedName) {
    throw new Error('Token name is required.');
  }

  const normalizedScopes = normalizeScopes(scopes);
  if (normalizedScopes.length === 0) {
    throw new Error('At least one valid scope is required.');
  }

  const tokenId = await generateUniqueTokenId(env);
  const tokenSecret = randomString(TOKEN_SECRET_LENGTH);
  const tokenSalt = randomString(TOKEN_SALT_LENGTH);
  const tokenHash = await hashTokenSecret(tokenSecret, tokenSalt);
  const tokenSuffix = tokenSecret.slice(-6);
  const now = Date.now();

  const record = {
    id: tokenId,
    name: normalizedName,
    scopes: normalizedScopes,
    expiresAt: normalizeExpiresAt(expiresAt),
    createdAt: now,
    lastUsedAt: null,
    enabled: enabled !== false,
    tokenSalt,
    tokenHash,
    tokenSuffix,
    tokenPreview: maskTokenSuffix(tokenSuffix),
  };

  await putRecord(record, env);

  return {
    token: `${TOKEN_PREFIX}${tokenId}_${tokenSecret}`,
    record: toPublicRecord(record),
  };
}

export async function listApiTokens(env) {
  ensureKvBinding(env);

  const keys = [];
  let cursor = undefined;
  let guard = 0;

  do {
    const page = await env.img_url.list({
      prefix: TOKEN_KEY_PREFIX,
      limit: 1000,
      cursor,
    });
    keys.push(...(page.keys || []).map((item) => item.name));
    cursor = page.list_complete ? undefined : page.cursor;
    guard += 1;
  } while (cursor && guard < 10000);

  const records = await Promise.all(
    keys.map(async (key) => {
      const id = key.slice(TOKEN_KEY_PREFIX.length);
      return getRecordById(id, env);
    })
  );

  return records
    .filter(Boolean)
    .sort((left, right) => Number(right.createdAt || 0) - Number(left.createdAt || 0))
    .map((record) => toPublicRecord(record));
}

export async function updateApiToken(tokenId, patch = {}, env) {
  const record = await getRecordById(tokenId, env);
  if (!record) return null;

  const next = { ...record };

  if (Object.prototype.hasOwnProperty.call(patch, 'enabled')) {
    next.enabled = Boolean(patch.enabled);
  }

  if (Object.prototype.hasOwnProperty.call(patch, 'name')) {
    const normalizedName = String(patch.name || '').trim();
    if (!normalizedName) {
      throw new Error('Token name is required.');
    }
    next.name = normalizedName;
  }

  if (Object.prototype.hasOwnProperty.call(patch, 'scopes')) {
    const normalizedScopes = normalizeScopes(patch.scopes);
    if (normalizedScopes.length === 0) {
      throw new Error('At least one valid scope is required.');
    }
    next.scopes = normalizedScopes;
  }

  if (Object.prototype.hasOwnProperty.call(patch, 'expiresAt')) {
    next.expiresAt = normalizeExpiresAt(patch.expiresAt);
  }

  await putRecord(next, env);
  return toPublicRecord(next);
}

export async function deleteApiToken(tokenId, env) {
  ensureKvBinding(env);
  const id = sanitizeTokenId(tokenId);
  if (!id) return false;
  const existing = await getRecordById(id, env);
  if (!existing) return false;
  await env.img_url.delete(`${TOKEN_KEY_PREFIX}${id}`);
  return true;
}

export async function touchApiTokenLastUsed(tokenId, env) {
  const record = await getRecordById(tokenId, env);
  if (!record) return false;
  record.lastUsedAt = Date.now();
  await putRecord(record, env);
  return true;
}

export async function verifyApiToken(tokenValue, env, requiredScope = '') {
  const split = splitToken(tokenValue);
  if (!split) {
    return {
      ok: false,
      status: 401,
      code: 'TOKEN_INVALID',
      message: 'API Token is invalid.',
    };
  }

  const record = await getRecordById(split.tokenId, env);
  if (!record) {
    return {
      ok: false,
      status: 401,
      code: 'TOKEN_INVALID',
      message: 'API Token is invalid.',
    };
  }

  const expectedHash = await hashTokenSecret(split.secret, record.tokenSalt || '');
  if (!timingSafeEqual(expectedHash, String(record.tokenHash || ''))) {
    return {
      ok: false,
      status: 401,
      code: 'TOKEN_INVALID',
      message: 'API Token is invalid.',
    };
  }

  if (!record.enabled) {
    return {
      ok: false,
      status: 401,
      code: 'TOKEN_DISABLED',
      message: 'API Token is disabled.',
    };
  }

  if (Number.isFinite(record.expiresAt) && record.expiresAt > 0 && Date.now() > record.expiresAt) {
    return {
      ok: false,
      status: 401,
      code: 'TOKEN_EXPIRED',
      message: 'API Token has expired.',
    };
  }

  const normalizedScope = String(requiredScope || '').trim().toLowerCase();
  if (normalizedScope && !record.scopes.includes(normalizedScope)) {
    return {
      ok: false,
      status: 403,
      code: 'TOKEN_SCOPE_DENIED',
      message: `API Token does not include "${normalizedScope}" scope.`,
    };
  }

  return {
    ok: true,
    token: toPublicRecord(record),
  };
}
