import crypto from 'node:crypto';
import type pg from 'pg';
import { hashSessionToken } from '../../core/auth-context.js';
import { config } from '../../core/config.js';
import { HttpError, getHeader, type RequestContext } from '../../core/http.js';

export interface PlatformUserSummary {
  id: string;
  username: string | null;
  phone: string | null;
  name: string | null;
  avatarUrl: string | null;
  primaryRole: string;
  createdAt: string;
}

export interface LoginSessionSummary {
  token: string;
  expiresAt: string;
}

export function clientIpFrom(ctx: RequestContext) {
  const forwarded = getHeader(ctx.req, 'x-forwarded-for');
  return (forwarded.split(',')[0] || getHeader(ctx.req, 'x-real-ip') || ctx.req.socket.remoteAddress || '').trim();
}

export function userAgentFrom(ctx: RequestContext) {
  return getHeader(ctx.req, 'user-agent');
}

export function normalizeChinaPhone(phone: string) {
  return phone.replace(/\s+/g, '').replace(/^\+?86/, '');
}

export function assertChinaPhone(phone: string) {
  const normalized = normalizeChinaPhone(phone);
  if (!/^1[3-9]\d{9}$/.test(normalized)) {
    throw new HttpError(400, 'Invalid China mainland phone number', 'INVALID_PHONE');
  }
  return normalized;
}

export function normalizePurpose(value: string) {
  if (['login', 'bind_phone', 'reset_password'].includes(value)) return value;
  throw new HttpError(400, 'Unsupported SMS purpose', 'UNSUPPORTED_SMS_PURPOSE');
}

export function generateSmsCode() {
  return crypto.randomInt(100000, 1000000).toString();
}

export function hashSmsCode(tenantId: string, phone: string, purpose: string, code: string) {
  return crypto
    .createHmac('sha256', config.authCodePepper)
    .update([tenantId, phone, purpose, code].join(':'))
    .digest('hex');
}

function stripUnsupportedOAuthProfileFields(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(item => stripUnsupportedOAuthProfileFields(item));
  if (!value || typeof value !== 'object') return value;
  const sanitized: Record<string, unknown> = {};
  for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
    const normalized = key.toLowerCase().replace(/[-_\s]/g, '');
    if (
      normalized.includes('accesstoken') ||
      normalized.includes('refreshtoken') ||
      normalized.includes('sessionkey') ||
      normalized.includes('secret') ||
      normalized.includes('clientsecret') ||
      normalized.includes('avatar') ||
      normalized.includes('headimg') ||
      normalized.includes('figureurl')
    ) {
      continue;
    }
    sanitized[key] = stripUnsupportedOAuthProfileFields(child);
  }
  return sanitized;
}

function publicOAuthProfile(profile: Record<string, unknown> | undefined) {
  const sanitized = stripUnsupportedOAuthProfileFields(profile || {});
  return sanitized && typeof sanitized === 'object' && !Array.isArray(sanitized)
    ? sanitized as Record<string, unknown>
    : {};
}

export function createSessionToken() {
  return `tk_${crypto.randomBytes(32).toString('base64url')}`;
}

export async function createLoginSession(
  client: pg.PoolClient,
  input: {
    tenantId: string;
    userId: string;
    provider: string;
    ipAddress?: string;
    userAgent?: string;
    metadata?: Record<string, unknown>;
  },
): Promise<LoginSessionSummary> {
  const token = createSessionToken();
  const tokenHash = hashSessionToken(token);
  const expiresAt = new Date(Date.now() + config.authSessionTtlSeconds * 1000).toISOString();

  await client.query(
    `
    insert into app_private.auth_sessions (
      tenant_id, user_id, token_hash, provider, expires_at, ip_address, user_agent, metadata
    )
    values ($1, $2, $3, $4, $5::timestamptz, $6, $7, $8::jsonb)
    `,
    [
      input.tenantId,
      input.userId,
      tokenHash,
      input.provider,
      expiresAt,
      input.ipAddress || null,
      input.userAgent || null,
      JSON.stringify(input.metadata || {}),
    ],
  );

  return { token, expiresAt };
}

export async function upsertPhoneUser(
  client: pg.PoolClient,
  input: {
    tenantId: string;
    phone: string;
  },
) {
  const existing = await client.query<PlatformUserSummary>(
    `
    select u.id, u.username, u.phone, u.name, u.avatar_url as "avatarUrl",
           u.primary_role as "primaryRole", u.created_at as "createdAt"
    from public.user_identities i
    join public.platform_users u on u.id = i.user_id
    where i.provider = 'phone'
      and i.provider_subject = $1
    limit 1
    `,
    [input.phone],
  );

  if (existing.rows[0]) {
    const user = existing.rows[0];
    await ensureStudentTenantRecords(client, input.tenantId, user.id);
    return { user, isNewUser: false };
  }

  const legacyPhoneUser = await client.query<PlatformUserSummary>(
    `
    select u.id, u.username, u.phone, u.name, u.avatar_url as "avatarUrl",
           u.primary_role as "primaryRole", u.created_at as "createdAt"
    from public.platform_users u
    where u.phone = $1
    order by u.created_at asc
    limit 1
    `,
    [input.phone],
  );

  if (legacyPhoneUser.rows[0]) {
    const user = legacyPhoneUser.rows[0];
    await client.query(
      `
      insert into public.user_identities (user_id, provider, provider_subject, phone)
      values ($1, 'phone', $2, $2)
      on conflict (provider, provider_subject)
      do update set user_id = excluded.user_id,
                    phone = excluded.phone,
                    updated_at = now()
      `,
      [user.id, input.phone],
    );
    await ensureStudentTenantRecords(client, input.tenantId, user.id);
    return { user, isNewUser: false };
  }

  const userResult = await client.query<PlatformUserSummary>(
    `
    insert into public.platform_users (username, phone, primary_role, raw_profile)
    values ($1, $2, 'student', $3::jsonb)
    returning id, username, phone, name, avatar_url as "avatarUrl",
              primary_role as "primaryRole", created_at as "createdAt"
    `,
    [`u_${input.phone.slice(-4)}_${Date.now().toString(36)}`, input.phone, JSON.stringify({ source: 'sms_login' })],
  );

  const user = userResult.rows[0];

  await client.query(
    `
    insert into public.user_identities (user_id, provider, provider_subject, phone)
    values ($1, 'phone', $2, $2)
    on conflict (provider, provider_subject)
    do update set user_id = excluded.user_id,
                  phone = excluded.phone,
                  updated_at = now()
    `,
    [user.id, input.phone],
  );

  await ensureStudentTenantRecords(client, input.tenantId, user.id);
  return { user, isNewUser: true };
}

export async function upsertOAuthUser(
  client: pg.PoolClient,
  input: {
    tenantId: string;
    provider: string;
    providerSubject: string;
    openId?: string | null;
    unionId?: string | null;
    phone?: string | null;
    email?: string | null;
    profile?: Record<string, unknown>;
    secretPayload?: Record<string, unknown>;
  },
) {
  const existing = await client.query<PlatformUserSummary>(
    `
    select u.id, u.username, u.phone, u.name, u.avatar_url as "avatarUrl",
           u.primary_role as "primaryRole", u.created_at as "createdAt"
    from public.user_identities i
    join public.platform_users u on u.id = i.user_id
    where i.provider = $1
      and i.provider_subject = $2
    limit 1
    `,
    [input.provider, input.providerSubject],
  );

  if (existing.rows[0]) {
    const user = existing.rows[0];
    await client.query(
      `
      update public.user_identities
      set union_id = coalesce($3, union_id),
          open_id = coalesce($4, open_id),
          phone = coalesce($5, phone),
          email = coalesce($6::citext, email),
          secret_payload = case
            when $7::jsonb = '{}'::jsonb then secret_payload
            else secret_payload || $7::jsonb
          end,
          updated_at = now()
      where provider = $1 and provider_subject = $2
      `,
      [
        input.provider,
        input.providerSubject,
        input.unionId || null,
        input.openId || null,
        input.phone || null,
        input.email || null,
        JSON.stringify(input.secretPayload || {}),
      ],
    );
    await ensureStudentTenantRecords(client, input.tenantId, user.id);
    return { user, isNewUser: false };
  }

  const unionMatch = input.unionId
    ? await client.query<PlatformUserSummary>(
        `
        select u.id, u.username, u.phone, u.name, u.avatar_url as "avatarUrl",
               u.primary_role as "primaryRole", u.created_at as "createdAt"
        from public.user_identities i
        join public.platform_users u on u.id = i.user_id
        where i.union_id = $1
          and i.provider in ('wechat-miniapp', 'wechat_miniapp', 'wechat_web')
        order by u.created_at asc
        limit 1
        `,
        [input.unionId],
      )
    : null;

  const phoneMatch =
    !unionMatch?.rows[0] && input.phone
      ? await client.query<PlatformUserSummary>(
          `
          select u.id, u.username, u.phone, u.name, u.avatar_url as "avatarUrl",
                 u.primary_role as "primaryRole", u.created_at as "createdAt"
          from public.platform_users u
          where u.phone = $1
          order by u.created_at asc
          limit 1
          `,
          [input.phone],
        )
      : null;

  const matchedUser = unionMatch?.rows[0] || phoneMatch?.rows[0] || null;
  if (matchedUser) {
    await client.query(
      `
      insert into public.user_identities (
        user_id, provider, provider_subject, union_id, open_id, phone, email, secret_payload
      )
      values ($1, $2, $3, $4, $5, $6, $7::citext, $8::jsonb)
      on conflict (provider, provider_subject)
      do update set user_id = excluded.user_id,
                    union_id = excluded.union_id,
                    open_id = excluded.open_id,
                    phone = excluded.phone,
                    email = excluded.email,
                    secret_payload = excluded.secret_payload,
                    updated_at = now()
      `,
      [
        matchedUser.id,
        input.provider,
        input.providerSubject,
        input.unionId || null,
        input.openId || null,
        input.phone || matchedUser.phone || null,
        input.email || null,
        JSON.stringify(input.secretPayload || {}),
      ],
    );
    await ensureStudentTenantRecords(client, input.tenantId, matchedUser.id);
    return { user: matchedUser, isNewUser: false };
  }

  const profile = publicOAuthProfile(input.profile);
  const nickname =
    typeof profile.nickname === 'string'
      ? profile.nickname
      : typeof profile.nickName === 'string'
        ? profile.nickName
        : typeof profile.name === 'string'
          ? profile.name
          : null;

  const userResult = await client.query<PlatformUserSummary>(
    `
    insert into public.platform_users (username, phone, email, name, primary_role, raw_profile)
    values ($1, $2, $3::citext, $4, 'student', $5::jsonb)
    returning id, username, phone, name, avatar_url as "avatarUrl",
              primary_role as "primaryRole", created_at as "createdAt"
    `,
    [
      `${input.provider.replace(/[^a-z0-9]/gi, '_')}_${crypto.randomBytes(5).toString('hex')}`,
      input.phone || null,
      input.email || null,
      nickname,
      JSON.stringify({
        source: input.provider,
        profile,
      }),
    ],
  );

  const user = userResult.rows[0];

  await client.query(
    `
    insert into public.user_identities (
      user_id, provider, provider_subject, union_id, open_id, phone, email, secret_payload
    )
    values ($1, $2, $3, $4, $5, $6, $7::citext, $8::jsonb)
    on conflict (provider, provider_subject)
    do update set user_id = excluded.user_id,
                  union_id = excluded.union_id,
                  open_id = excluded.open_id,
                  phone = excluded.phone,
                  email = excluded.email,
                  secret_payload = excluded.secret_payload,
                  updated_at = now()
    `,
    [
      user.id,
      input.provider,
      input.providerSubject,
      input.unionId || null,
      input.openId || null,
      input.phone || null,
      input.email || null,
      JSON.stringify(input.secretPayload || {}),
    ],
  );

  await ensureStudentTenantRecords(client, input.tenantId, user.id);
  return { user, isNewUser: true };
}

export async function ensureStudentTenantRecords(client: pg.PoolClient, tenantId: string, userId: string) {
  await client.query(
    `
    insert into public.tenant_memberships (tenant_id, user_id, role, status)
    values ($1, $2, 'student', 'active')
    on conflict (tenant_id, user_id, role)
    do update set status = 'active', updated_at = now()
    `,
    [tenantId, userId],
  );

  await client.query(
    `
    insert into public.student_profiles (tenant_id, user_id, stats, progress)
    values ($1, $2, $3::jsonb, '{}'::jsonb)
    on conflict (tenant_id, user_id) do nothing
    `,
    [
      tenantId,
      userId,
      JSON.stringify({
        totalAnswered: 0,
        correctCount: 0,
        wrongCount: 0,
        studyDays: 1,
      }),
    ],
  );
}

export async function writeLoginEvent(
  client: pg.PoolClient,
  input: {
    tenantId: string;
    userId?: string | null;
    provider: string;
    identifier?: string;
    result: 'sent' | 'success' | 'failed' | 'blocked';
    failureCode?: string | null;
    ipAddress?: string;
    userAgent?: string;
    metadata?: Record<string, unknown>;
  },
) {
  await client.query(
    `
    insert into public.auth_login_events (
      tenant_id, user_id, provider, identifier, result, failure_code,
      ip_address, user_agent, metadata
    )
    values ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
    `,
    [
      input.tenantId,
      input.userId || null,
      input.provider,
      input.identifier || null,
      input.result,
      input.failureCode || null,
      input.ipAddress || null,
      input.userAgent || null,
      JSON.stringify(input.metadata || {}),
    ],
  );
}
