// auth.ts
// Scope: local session, token files, and normalized server authority.
// Server owns licensing, usage, limits, packages, prices, and run authorization.

import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
import {
  USER_PATH,
  AUTH_TOKEN_PATH as TOKEN_PATH,
  AUTH_ACCESS_PATH as ACCESS_PATH,
  AUTH_REFRESH_PATH as REFRESH_PATH
} from "../pathList";

export type UserTier = "limited" | "unlimited" | "backstage";

export type TelemetryConsent = {
  structural: boolean;
  logs: boolean;
  updatedAt: number;
};

export type AuthorityPack = {
  tier: UserTier;
  access: string;
  licensed: boolean;
  licenseStatus: string;
  packageName: string;
  price: string;
  limitRemaining: number;
  policyVersion: string;
  features: Record<string, boolean>;
  authenticated: boolean;
  manifestVersion: string;
  updateChannel: string;
  expiresAt: string;
  serverTime: string;
  message: string;
  reason: string;
  checkedAt: number;
};

export type UserRecord = {
  userId: string;
  email: string;

  // Mirrors kept for older callers. AuthorityPack is the real local state.
  tier: UserTier;
  access: string;
  licensed: boolean;
  licenseStatus: string;
  packageName: string;
  price: string;

  sessionToken: string;
  tokenExpiresAt: number;
  remoteAccessToken: string;
  remoteRefreshToken: string;
  authority: AuthorityPack;

  telemetry: TelemetryConsent;
  installedAt: number;
  lastAuthAt: number;
  licenseKey: string;
};

export type AuthResult = {
  ok: boolean;
  userId?: string;
  tier?: UserTier;
  licensed?: boolean;
  error?: string;
};

export type RunLimitState = {
  limitRemaining: number;
  policyVersion: string;
  checkedAt: number;
};

const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const POLICY_VERSION = "run-authority-v1";

export const DEFAULT_AUTHORITY: AuthorityPack = {
  tier: "limited",
  access: "limited",
  licensed: false,
  licenseStatus: "unlicensed",
  packageName: "",
  price: "",
  limitRemaining: 0,
  policyVersion: POLICY_VERSION,
  features: {},
  authenticated: false,
  manifestVersion: "",
  updateChannel: "",
  expiresAt: "",
  serverTime: "",
  message: "",
  reason: "",
  checkedAt: 0
};

function guestUser(): UserRecord {
  const now = Date.now();
  return {
    userId: "",
    email: "",
    tier: "limited",
    access: "limited",
    licensed: false,
    licenseStatus: "unlicensed",
    packageName: "",
    price: "",
    sessionToken: "",
    tokenExpiresAt: 0,
    remoteAccessToken: "",
    remoteRefreshToken: "",
    authority: { ...DEFAULT_AUTHORITY },
    telemetry: { structural: true, logs: true, updatedAt: 0 },
    installedAt: now,
    lastAuthAt: 0,
    licenseKey: ""
  };
}

const str = (v: unknown, f = ""): string => v === undefined || v === null ? f : String(v);
const num = (v: unknown, f = 0): number => Number.isFinite(Number(v)) ? Number(v) : f;
const bool = (v: unknown, f = false): boolean => typeof v === "boolean" ? v : f;

export function normalizeTier(value: unknown): UserTier {
  const v = String(value || "").trim().toLowerCase();
  if (v === "backstage") return "backstage";
  if (v === "unlimited" || v === "standard" || v === "custom" || v === "lifetime_custom") return "unlimited";
  return "limited";
}

export const normalizeAccessTier = normalizeTier;

function normalizeFeatures(value: unknown, fallback: Record<string, boolean>): Record<string, boolean> {
  if (!value || typeof value !== "object") return { ...fallback };
  return Object.fromEntries(
    Object.entries(value as Record<string, unknown>).map(([k, v]) => [k, Boolean(v)])
  );
}

export function makeAuthorityPack(raw: any = {}, fallback: Partial<AuthorityPack> = {}): AuthorityPack {
  const base = { ...DEFAULT_AUTHORITY, ...fallback };
  const tier = normalizeTier(raw.tier ?? raw.accessTier ?? raw.access ?? base.tier);

  return {
    tier,
    access: str(raw.access ?? raw.accessTier, base.access || tier),
    licensed: bool(raw.licensed, base.licensed),
    licenseStatus: str(raw.licenseStatus, base.licenseStatus),
    packageName: str(raw.packageName ?? raw.package, base.packageName),
    price: str(raw.price, base.price),
    limitRemaining: num(raw.limitRemaining ?? raw.limit_remaining, base.limitRemaining),
    policyVersion: str(raw.policyVersion, base.policyVersion || POLICY_VERSION),
    features: normalizeFeatures(raw.features, base.features || {}),
    authenticated: bool(raw.authenticated, base.authenticated),
    manifestVersion: str(raw.manifestVersion, base.manifestVersion),
    updateChannel: str(raw.updateChannel, base.updateChannel),
    expiresAt: str(raw.expiresAt, base.expiresAt),
    serverTime: str(raw.serverTime, base.serverTime),
    message: str(raw.message, base.message),
    reason: str(raw.reason, base.reason),
    checkedAt: num(raw.checkedAt, base.checkedAt || Date.now())
  };
}

function withAuthorityMirrors(user: UserRecord): UserRecord {
  const authority = makeAuthorityPack(user.authority, user.authority);
  return {
    ...user,
    authority,
    tier: authority.tier,
    access: authority.access,
    licensed: authority.licensed,
    licenseStatus: authority.licenseStatus,
    packageName: authority.packageName,
    price: authority.price
  };
}

function ensureParent(filePath: string): void {
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
}

function readUser(): UserRecord {
  try {
    const raw = JSON.parse(fs.readFileSync(USER_PATH, "utf8"));
    const legacyAuthority = {
      tier: raw.tier ?? raw.accessTier,
      access: raw.access,
      licensed: raw.licensed,
      licenseStatus: raw.licenseStatus,
      packageName: raw.packageName ?? raw.package,
      price: raw.price
    };

    return withAuthorityMirrors({
      ...guestUser(),
      ...raw,
      installedAt: num(raw.installedAt, Date.now()),
      telemetry: {
        structural: true,
        logs: bool(raw.telemetry?.logs, true),
        updatedAt: num(raw.telemetry?.updatedAt, 0)
      },
      authority: makeAuthorityPack(raw.authority ?? raw.runtimeAuthority ?? legacyAuthority, legacyAuthority)
    });
  } catch {
    return guestUser();
  }
}

function writeUser(user: UserRecord): void {
  ensureParent(USER_PATH);
  fs.writeFileSync(USER_PATH, JSON.stringify(withAuthorityMirrors(user), null, 2), "utf8");
}

function writeText(filePath: string, value: string): void {
  try {
    ensureParent(filePath);
    fs.writeFileSync(filePath, value, "utf8");
  } catch {}
}

function removeFile(filePath: string): void {
  try { fs.unlinkSync(filePath); } catch {}
}

function syncTokenFiles(user: UserRecord): void {
  user.sessionToken ? writeText(TOKEN_PATH, user.sessionToken) : removeFile(TOKEN_PATH);
  user.remoteAccessToken ? writeText(ACCESS_PATH, user.remoteAccessToken) : removeFile(ACCESS_PATH);
  user.remoteRefreshToken ? writeText(REFRESH_PATH, user.remoteRefreshToken) : removeFile(REFRESH_PATH);
}

function validSession(user = readUser()): boolean {
  return Boolean(user.sessionToken && user.tokenExpiresAt && Date.now() < user.tokenExpiresAt);
}

export function getUser(): UserRecord {
  return readUser();
}

export function getAuthority(): AuthorityPack {
  return readUser().authority;
}

export const getRuntimeAuthority = getAuthority;

export function setAuthority(update: Partial<AuthorityPack>): AuthorityPack {
  const user = readUser();
  const authority = makeAuthorityPack(update, user.authority);
  writeUser({ ...user, authority });
  return authority;
}

export const setRuntimeAuthority = setAuthority;

export function establishPortalSession(payload: any = {}): UserRecord {
  const remoteUser = payload?.user && typeof payload.user === "object" ? payload.user : {};
  const userId = str(remoteUser.id ?? remoteUser.userId ?? payload.userId ?? payload.id);
  const email = str(remoteUser.email ?? payload.email);
  const accessToken = str(payload.accessToken ?? payload.remoteAccessToken);
  const refreshToken = str(payload.refreshToken ?? payload.remoteRefreshToken);

  if (!userId || !email || !accessToken || !refreshToken) {
    throw new Error("invalid_portal_auth_payload");
  }

  const authority = makeAuthorityPack({
    tier: payload.tier ?? payload.access ?? remoteUser.tier ?? remoteUser.access,
    access: payload.access ?? payload.tier ?? remoteUser.access ?? remoteUser.tier,
    licensed: payload.licensed,
    licenseStatus: payload.licenseStatus,
    packageName: payload.packageName ?? payload.package ?? remoteUser.packageName ?? remoteUser.package,
    price: payload.price ?? remoteUser.price,
    limitRemaining: payload.limitRemaining ?? payload.limit_remaining ?? remoteUser.limitRemaining ?? remoteUser.limit_remaining,
    policyVersion: payload.policyVersion,
    features: payload.features,
    authenticated: true,
    manifestVersion: payload.manifestVersion,
    updateChannel: payload.updateChannel,
    expiresAt: payload.expiresAt,
    serverTime: payload.serverTime,
    message: payload.message || "portal_sign_in",
    reason: payload.reason || "portal_return",
    checkedAt: Date.now()
  }, getAuthority());

  return establishSession({
    userId,
    email,
    authority,
    remoteAccessToken: accessToken,
    remoteRefreshToken: refreshToken,
    licenseKey: str(payload.licenseKey ?? remoteUser.licenseKey ?? remoteUser.license_key),
    telemetry: payload.telemetry
  });
}

export function getSessionTier(): UserTier {
  const user = readUser();
  return validSession(user) ? user.tier : "limited";
}

let runtimeTierOverride: UserTier | null = null;

export function getEffectiveRuntimeTier(): UserTier {
  const user = readUser();
  if (!validSession(user)) return "limited";
  return runtimeTierOverride ?? user.authority.tier;
}

export function getUserTier(): UserTier {
  return getEffectiveRuntimeTier();
}

// Runtime only. Does not write user.json and does not change stored server authority.
export function setEffectiveRuntimeTier(tier: UserTier): AuthorityPack {
  runtimeTierOverride = normalizeTier(tier);

  return makeAuthorityPack({
    ...getAuthority(),
    tier: runtimeTierOverride,
    access: runtimeTierOverride,
    checkedAt: Date.now()
  }, getAuthority());
}

// Runtime only. Do not persist fallback tier into the account file.
export function setRuntimeUserTier(tier: UserTier): void {
  runtimeTierOverride = normalizeTier(tier);
}

export function getRunLimitState(): RunLimitState {
  const a = getAuthority();
  return {
    limitRemaining: a.limitRemaining,
    policyVersion: a.policyVersion,
    checkedAt: a.checkedAt
  };
}

export function setRunLimitState(update: Partial<RunLimitState> & { message?: string; reason?: string }): RunLimitState {
  const a = setAuthority({
    limitRemaining: update.limitRemaining,
    policyVersion: update.policyVersion,
    message: update.message,
    reason: update.reason,
    checkedAt: Date.now()
  });

  return {
    limitRemaining: a.limitRemaining,
    policyVersion: a.policyVersion,
    checkedAt: a.checkedAt
  };
}

export function isAuthenticated(): boolean {
  return validSession();
}

export function isLicensed(): boolean {
  const user = readUser();
  return validSession(user) && user.authority.licensed;
}

export function getTelemetryConsent(): TelemetryConsent {
  return readUser().telemetry;
}

export function validateRequestToken(token: string): AuthResult {
  const user = readUser();

  if (!token && !user.sessionToken && !user.userId) {
    return { ok: true, tier: "limited", licensed: false };
  }

  if (!token) return { ok: false, error: "missing_token" };
  if (token !== user.sessionToken) return { ok: false, error: "invalid_token" };
  if (!validSession(user)) return { ok: false, error: "token_expired" };

  return {
    ok: true,
    userId: user.userId,
    tier: user.authority.tier,
    licensed: user.authority.licensed
  };
}

export function establishSession(params: {
  userId: string;
  email: string;
  authority: AuthorityPack;
  remoteAccessToken?: string;
  remoteRefreshToken?: string;
  licenseKey?: string;
  telemetry?: Partial<TelemetryConsent>;
}): UserRecord {
  runtimeTierOverride = null;

  const existing = readUser();
  const now = Date.now();

  const authority = makeAuthorityPack({ ...params.authority, checkedAt: now }, existing.authority);

  const user = withAuthorityMirrors({
    ...existing,
    userId: params.userId,
    email: params.email,
    sessionToken: crypto.randomBytes(32).toString("hex"),
    tokenExpiresAt: now + SESSION_TTL_MS,
    remoteAccessToken: str(params.remoteAccessToken, existing.remoteAccessToken),
    remoteRefreshToken: str(params.remoteRefreshToken, existing.remoteRefreshToken),
    authority,
    telemetry: {
      structural: true,
      logs: bool(params.telemetry?.logs, existing.telemetry.logs),
      updatedAt: now
    },
    installedAt: existing.installedAt || now,
    lastAuthAt: now,
    licenseKey: str(params.licenseKey, existing.licenseKey)
  });

  writeUser(user);
  syncTokenFiles(user);
  return user;
}

export function clearSession(): void {
  runtimeTierOverride = null;
  
  const existing = readUser();
  const user = withAuthorityMirrors({
    ...existing,
    sessionToken: "",
    tokenExpiresAt: 0,
    remoteAccessToken: "",
    remoteRefreshToken: "",
    authority: makeAuthorityPack({
      ...DEFAULT_AUTHORITY,
      message: "session_cleared",
      reason: "clear_session",
      checkedAt: Date.now()
    }),
    lastAuthAt: 0
  });

  writeUser(user);
  syncTokenFiles(user);
}

export function getCachedToken(): string {
  try { return fs.readFileSync(TOKEN_PATH, "utf8").trim(); } catch { return ""; }
}

export function getRemoteAccessToken(): string {
  try { return fs.readFileSync(ACCESS_PATH, "utf8").trim(); }
  catch { return readUser().remoteAccessToken || ""; }
}

export function getRemoteRefreshToken(): string {
  try { return fs.readFileSync(REFRESH_PATH, "utf8").trim(); }
  catch { return readUser().remoteRefreshToken || ""; }
}

export function setTelemetryConsent(logs: boolean): TelemetryConsent {
  const user = readUser();
  const telemetry = { structural: true, logs, updatedAt: Date.now() };
  writeUser({ ...user, telemetry });
  return telemetry;
}

export function ensureUserFile(): void {
  if (!fs.existsSync(USER_PATH)) writeUser(guestUser());
}