import crypto from 'node:crypto';
import type pg from 'pg';
import { pool } from '../db.js';
import { config } from '../config.js';

const PROVIDERS = ['generic', 'dingtalk', 'feishu', 'wecom'] as const;
type CrmProvider = typeof PROVIDERS[number];

interface CrmQueueRow {
  id: string;
  tenantId: string;
  recordId: string | null;
  status: string;
  attempts: number;
  leadId: string | null;
  source: string | null;
  payload: Record<string, unknown>;
  targetUrl: string | null;
}

interface CrmConfigRow {
  tenantId: string;
  enabled: boolean;
  url: string | null;
  secretRef: string | null;
  formName: string | null;
  examType: string | null;
  timeoutSec: number | null;
}

interface SecretRow {
  secretValue: string | null;
  secretJson: Record<string, unknown> | null;
}

interface PreparedRequest {
  provider: CrmProvider;
  url: string;
  body: Record<string, unknown>;
  headers: Record<string, string>;
}

interface SendResult {
  ok: boolean;
  httpCode: number;
  responseSummary: string;
  errorMessage: string;
}

interface ProcessResult {
  processed: number;
  sent: number;
  failed: number;
  retrying: number;
  discarded: number;
}

function objectValue(value: unknown): Record<string, unknown> {
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function stringValue(value: unknown, fallback = '') {
  return typeof value === 'string' && value.trim() ? value.trim() : fallback;
}

function numberValue(value: unknown, fallback = 0) {
  const parsed = Number(value ?? fallback);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function providerFrom(configRow: CrmConfigRow | null, queueRow: CrmQueueRow): CrmProvider {
  const payload = objectValue(queueRow.payload);
  const payloadProvider = stringValue(payload.provider);
  if ((PROVIDERS as readonly string[]).includes(payloadProvider)) return payloadProvider as CrmProvider;
  return providerFromUrl(queueRow.targetUrl || configRow?.url || '');
}

function providerFromUrl(url: string) {
  const lower = url.toLowerCase();
  if (lower.includes('dingtalk.com')) return 'dingtalk';
  if (lower.includes('feishu.cn') || lower.includes('larksuite.com')) return 'feishu';
  if (lower.includes('qyapi.weixin.qq.com') || lower.includes('work.weixin.qq.com')) return 'wecom';
  return 'generic';
}

function parseSecretRef(ref: string | null) {
  if (!ref) return null;
  const parts = ref.split(':');
  if (parts.length !== 3 || parts[0] !== 'app_private.tenant_secrets') return null;
  return { scope: parts[1], key: parts[2] };
}

function secretText(secret: SecretRow | null, keys: string[]) {
  if (!secret) return '';
  if (secret.secretValue?.trim()) return secret.secretValue.trim();
  const json = objectValue(secret.secretJson);
  for (const key of keys) {
    const value = json[key];
    if (typeof value === 'string' && value.trim()) return value.trim();
  }
  return '';
}

function truncate(value: unknown, max = 1900) {
  return String(value ?? '').slice(0, max);
}

function validateWebhookUrl(rawUrl: string) {
  let url: URL;
  try {
    url = new URL(rawUrl);
  } catch {
    throw new Error('CRM webhook URL is invalid');
  }
  const isLocalhost = ['127.0.0.1', 'localhost', '::1'].includes(url.hostname);
  if (url.protocol !== 'https:' && !(config.crmAllowInsecureLocalhost && isLocalhost)) {
    throw new Error('CRM webhook URL must use HTTPS outside local development');
  }
  url.username = '';
  url.password = '';
  return url;
}

function compactLeadPayload(task: CrmQueueRow, configRow: CrmConfigRow | null) {
  const payload = objectValue(task.payload);
  const student = objectValue(payload.student);
  const referral = objectValue(payload.referral);
  return {
    formName: stringValue(payload.formName, configRow?.formName || '刷题题库'),
    examType: stringValue(payload.examType, configRow?.examType || '成人本科'),
    source: stringValue(payload.source, task.source || 'unknown'),
    leadId: stringValue(payload.leadId, task.leadId || ''),
    student: {
      id: stringValue(student.id, task.recordId || ''),
      name: stringValue(student.name) || stringValue(student.username) || '未填写',
      phone: stringValue(student.phone),
      email: stringValue(student.email),
    },
    referral: {
      refCode: stringValue(referral.refCode),
      referrerUserId: stringValue(referral.referrerUserId),
      boundAt: stringValue(referral.boundAt),
    },
    metadata: objectValue(payload.metadata),
  };
}

function arrayValue(value: unknown) {
  return Array.isArray(value) ? value : [];
}

function compactStudentCrmPushPayload(task: CrmQueueRow, configRow: CrmConfigRow | null) {
  const payload = objectValue(task.payload);
  const student = objectValue(payload.student);
  const assignee = objectValue(payload.assignee);
  const classInfo = objectValue(payload.class);
  const followup = objectValue(payload.followup);
  const classes = arrayValue(student.classes).slice(0, 5).map(item => {
    const classItem = objectValue(item);
    return {
      classId: stringValue(classItem.classId),
      className: stringValue(classItem.className),
      classCode: stringValue(classItem.classCode),
    };
  });

  return {
    formName: stringValue(payload.formName, configRow?.formName || '刷题题库'),
    examType: stringValue(payload.examType, configRow?.examType || '成人本科'),
    source: stringValue(payload.source, task.source || 'manual_batch'),
    title: stringValue(payload.title, '学生跟进提醒'),
    message: stringValue(payload.message),
    student: {
      id: stringValue(student.userId) || stringValue(student.id, task.recordId || ''),
      username: stringValue(student.username),
      name: stringValue(student.name) || stringValue(student.username) || '未填写',
      phone: stringValue(student.phone),
      email: stringValue(student.email),
      avatarPreset: stringValue(student.avatarPreset, 'male'),
      regionName: stringValue(student.regionName),
      selectedSchoolName: stringValue(student.selectedSchoolName),
      selectedMajorName: stringValue(student.selectedMajorName),
      classes,
    },
    assignee: stringValue(assignee.userId) || stringValue(assignee.id) ? {
      id: stringValue(assignee.userId) || stringValue(assignee.id),
      name: stringValue(assignee.name) || stringValue(assignee.username),
      role: stringValue(assignee.role),
    } : null,
    class: stringValue(classInfo.classId) ? {
      classId: stringValue(classInfo.classId),
      className: stringValue(classInfo.className),
      classCode: stringValue(classInfo.classCode),
    } : null,
    followup: {
      id: stringValue(followup.id),
      title: stringValue(followup.title) || stringValue(payload.title),
      followupType: stringValue(followup.followupType),
      priority: stringValue(followup.priority),
      dueAt: stringValue(followup.dueAt),
    },
    metadata: objectValue(payload.metadata),
  };
}

function leadMarkdown(task: CrmQueueRow, configRow: CrmConfigRow | null) {
  const payload = compactLeadPayload(task, configRow);
  const student = payload.student;
  const lines = [
    `### ${payload.formName}新客资`,
    `- 考试类型：${payload.examType}`,
    `- 来源：${payload.source}`,
    `- 学生：${student.name}`,
    `- 手机：${student.phone || '未填写'}`,
    `- 推广码：${payload.referral.refCode || '无'}`,
    `- 归属人ID：${payload.referral.referrerUserId || '无'}`,
    `- 绑定时间：${payload.referral.boundAt || '无'}`,
    `- 线索ID：${payload.leadId}`,
  ];
  return lines.join('\n');
}

function studentCrmPushMarkdown(task: CrmQueueRow, configRow: CrmConfigRow | null) {
  const payload = compactStudentCrmPushPayload(task, configRow);
  const student = payload.student;
  const target = [student.regionName, student.selectedSchoolName, student.selectedMajorName].filter(Boolean).join(' / ');
  const classes = student.classes
    .map(item => item.className || item.classCode)
    .filter(Boolean)
    .join('、');
  const lines = [
    `### ${payload.formName}学生跟进`,
    `- 考试类型：${payload.examType}`,
    `- 来源：${payload.source}`,
    `- 任务：${payload.title}`,
    `- 学生：${student.name}`,
    `- 手机：${student.phone || '未填写'}`,
    `- 意向：${target || '未填写'}`,
    `- 班级：${payload.class?.className || classes || '无'}`,
    `- 指派：${payload.assignee?.name || payload.assignee?.id || '未指派'}`,
    `- 优先级：${payload.followup.priority || 'normal'}`,
    `- 到期时间：${payload.followup.dueAt || '无'}`,
    `- 跟进ID：${payload.followup.id || '无'}`,
  ];
  if (payload.message) lines.splice(4, 0, `- 说明：${payload.message}`);
  return lines.join('\n');
}

function taskEventType(task: CrmQueueRow) {
  return stringValue(objectValue(task.payload).eventType, 'lead.created');
}

function dingtalkSign(secret: string): Record<string, string> {
  if (!secret) return {};
  const timestamp = String(Date.now());
  const sign = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}\n${secret}`)
    .digest('base64');
  return { timestamp, sign };
}

function feishuSign(secret: string): Record<string, string> {
  if (!secret) return {};
  const timestamp = String(Math.floor(Date.now() / 1000));
  const sign = crypto
    .createHmac('sha256', `${timestamp}\n${secret}`)
    .update('')
    .digest('base64');
  return { timestamp, sign };
}

function appendQuery(url: URL, params: Record<string, string>) {
  for (const [key, value] of Object.entries(params)) {
    if (value) url.searchParams.set(key, value);
  }
  return url.toString();
}

function prepareRequest(task: CrmQueueRow, configRow: CrmConfigRow | null, secret: SecretRow | null): PreparedRequest {
  const target = validateWebhookUrl(task.targetUrl || configRow?.url || '');
  const provider = providerFrom(configRow, task);
  const secretValue = secretText(secret, ['secret', 'signSecret', 'webhookSecret']);
  const eventType = taskEventType(task);
  const isStudentPush = eventType === 'student.crm_push';
  const markdown = isStudentPush ? studentCrmPushMarkdown(task, configRow) : leadMarkdown(task, configRow);
  const title = isStudentPush ? '学生跟进提醒' : '新客资提醒';
  const headers = { 'content-type': 'application/json' };

  if (provider === 'dingtalk') {
    return {
      provider,
      url: appendQuery(target, dingtalkSign(secretValue)),
      headers,
      body: {
        msgtype: 'markdown',
        markdown: {
          title,
          text: markdown,
        },
      },
    };
  }

  if (provider === 'feishu') {
    return {
      provider,
      url: target.toString(),
      headers,
      body: {
        msg_type: 'interactive',
        ...feishuSign(secretValue),
        card: {
          config: { wide_screen_mode: true },
          header: { title: { tag: 'plain_text', content: title }, template: isStudentPush ? 'wathet' : 'blue' },
          elements: [{ tag: 'markdown', content: markdown }],
        },
      },
    };
  }

  if (provider === 'wecom') {
    return {
      provider,
      url: target.toString(),
      headers,
      body: {
        msgtype: 'markdown',
        markdown: { content: markdown },
      },
    };
  }

  return {
    provider,
    url: target.toString(),
    headers,
    body: isStudentPush ? {
      event: 'student.crm_push',
      data: compactStudentCrmPushPayload(task, configRow),
    } : {
      event: 'lead.created',
      data: compactLeadPayload(task, configRow),
    },
  };
}

async function loadCrmConfig(client: pg.PoolClient, tenantId: string) {
  const result = await client.query<CrmConfigRow>(
    `
    select tenant_id as "tenantId", enabled, url, secret_ref as "secretRef",
           form_name as "formName", exam_type as "examType", timeout_sec as "timeoutSec"
    from public.crm_config
    where tenant_id = $1
    limit 1
    `,
    [tenantId],
  );
  return result.rows[0] || null;
}

async function loadSecret(client: pg.PoolClient, tenantId: string, secretRef: string | null) {
  const parsed = parseSecretRef(secretRef);
  if (!parsed) return null;
  const result = await client.query<SecretRow>(
    `
    select secret_value as "secretValue", secret_json as "secretJson"
    from app_private.tenant_secrets
    where tenant_id = $1
      and secret_scope = $2
      and secret_key = $3
    limit 1
    `,
    [tenantId, parsed.scope, parsed.key],
  );
  return result.rows[0] || null;
}

async function claimDueTasks(client: pg.PoolClient, limit: number) {
  const result = await client.query<CrmQueueRow>(
    `
    with due as (
      select id
      from public.crm_webhook_queue
      where status in ('pending', 'retrying')
        and coalesce(next_attempt_at, scheduled_at, created_at) <= now()
      order by coalesce(next_attempt_at, scheduled_at, created_at) asc, created_at asc
      limit $1
      for update skip locked
    )
    update public.crm_webhook_queue q
    set status = 'processing',
        last_attempt_at = now(),
        updated_at = now()
    from due
    where q.id = due.id
    returning q.id,
              q.tenant_id as "tenantId",
              q.record_id as "recordId",
              q.status,
              q.attempts,
              q.lead_id as "leadId",
              q.source,
              q.payload,
              q.target_url as "targetUrl"
    `,
    [limit],
  );
  return result.rows;
}

async function recoverStaleProcessingTasks(client: pg.PoolClient) {
  const staleMs = Math.max(config.crmRequestTimeoutMs * 3, 60_000);
  await client.query(
    `
    update public.crm_webhook_queue
    set status = 'retrying',
        next_attempt_at = now(),
        last_error = coalesce(last_error, 'Recovered stale processing task'),
        updated_at = now()
    where status = 'processing'
      and coalesce(last_attempt_at, updated_at, created_at) < now() - ($1::int * interval '1 millisecond')
    `,
    [staleMs],
  );
}

async function sendWebhook(request: PreparedRequest, timeoutMs: number): Promise<SendResult> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetch(request.url, {
      method: 'POST',
      headers: request.headers,
      body: JSON.stringify(request.body),
      signal: controller.signal,
    });
    const text = await response.text().catch(() => '');
    return {
      ok: response.ok,
      httpCode: response.status,
      responseSummary: truncate(text, 1900),
      errorMessage: response.ok ? '' : `CRM webhook returned HTTP ${response.status}`,
    };
  } catch (error) {
    return {
      ok: false,
      httpCode: 0,
      responseSummary: '',
      errorMessage: error instanceof Error ? error.message : String(error),
    };
  } finally {
    clearTimeout(timeout);
  }
}

async function appendLog(
  client: pg.PoolClient,
  task: CrmQueueRow,
  request: PreparedRequest | null,
  result: SendResult,
  attempt: number,
) {
  await client.query(
    `
    insert into public.crm_webhook_log (
      tenant_id, queue_id, record_id, http_code, outcome, error_message, lead_id,
      request_body, request_payload, response_summary, signed_at, attempt
    )
    values ($1, $2::uuid, $3, $4, $5, $6, $7, $8, $9::jsonb, $10, now(), $11)
    `,
    [
      task.tenantId,
      task.id,
      task.recordId,
      result.httpCode,
      result.ok ? 'sent' : 'failed',
      truncate(result.errorMessage),
      task.leadId,
      request ? JSON.stringify(request.body).slice(0, 19_000) : '',
      JSON.stringify({
        provider: request?.provider || null,
        urlHost: request ? new URL(request.url).hostname : null,
        body: request?.body || null,
      }),
      truncate(result.responseSummary),
      attempt,
    ],
  );
}

async function markTaskResult(
  client: pg.PoolClient,
  task: CrmQueueRow,
  request: PreparedRequest | null,
  result: SendResult,
  attempt: number,
) {
  const backoff = config.crmBackoffSeconds[Math.min(attempt - 1, config.crmBackoffSeconds.length - 1)] || 60;
  if (result.ok) {
    await client.query(
      `
      update public.crm_webhook_queue
      set status = 'sent',
          attempts = $2,
          provider = $3,
          next_attempt_at = null,
          last_error = null,
          last_http_code = $4,
          last_response_summary = $5,
          sent_at = now(),
          updated_at = now()
      where id = $1
      `,
      [task.id, attempt, request?.provider || null, result.httpCode, truncate(result.responseSummary)],
    );
    return 'sent';
  }

  const terminal = attempt >= config.crmMaxAttempts;
  await client.query(
    `
    update public.crm_webhook_queue
    set status = $2,
        attempts = $3,
        provider = $4,
        next_attempt_at = case when $2 = 'retrying' then now() + ($5::int * interval '1 second') else null end,
        dead_lettered_at = case when $2 = 'failed' then coalesce(dead_lettered_at, now()) else dead_lettered_at end,
        last_error = $6,
        last_http_code = $7,
        last_response_summary = $8,
        updated_at = now()
    where id = $1
    `,
    [
      task.id,
      terminal ? 'failed' : 'retrying',
      attempt,
      request?.provider || null,
      backoff,
      truncate(result.errorMessage),
      result.httpCode,
      truncate(result.responseSummary),
    ],
  );
  return terminal ? 'failed' : 'retrying';
}

async function discardTask(client: pg.PoolClient, task: CrmQueueRow, message: string) {
  await client.query(
    `
    update public.crm_webhook_queue
    set status = 'discarded',
        attempts = attempts + 1,
        last_attempt_at = now(),
        dead_lettered_at = coalesce(dead_lettered_at, now()),
        last_error = $2,
        updated_at = now()
    where id = $1
    `,
    [task.id, truncate(message)],
  );
  await appendLog(client, task, null, {
    ok: false,
    httpCode: 0,
    responseSummary: '',
    errorMessage: message,
  }, task.attempts + 1);
}

async function processTask(task: CrmQueueRow) {
  const client = await pool.connect();
  try {
    const crmConfig = await loadCrmConfig(client, task.tenantId);
    if (!crmConfig?.enabled || !(task.targetUrl || crmConfig.url)) {
      await discardTask(client, task, 'CRM config is disabled or missing URL');
      return 'discarded';
    }
    const secret = await loadSecret(client, task.tenantId, crmConfig.secretRef);
    const request = prepareRequest(task, crmConfig, secret);
    const timeoutMs = Math.max(1000, (crmConfig.timeoutSec || 0) * 1000 || config.crmRequestTimeoutMs);
    const attempt = task.attempts + 1;
    const result = await sendWebhook(request, timeoutMs);
    await appendLog(client, task, request, result, attempt);
    return markTaskResult(client, task, request, result, attempt);
  } catch (error) {
    const attempt = task.attempts + 1;
    const result: SendResult = {
      ok: false,
      httpCode: 0,
      responseSummary: '',
      errorMessage: error instanceof Error ? error.message : String(error),
    };
    await appendLog(client, task, null, result, attempt);
    return markTaskResult(client, task, null, result, attempt);
  } finally {
    client.release();
  }
}

export async function processCrmBatch(limit = config.crmBatchSize): Promise<ProcessResult> {
  const client = await pool.connect();
  let tasks: CrmQueueRow[] = [];
  try {
    await client.query('begin');
    await recoverStaleProcessingTasks(client);
    tasks = await claimDueTasks(client, limit);
    await client.query('commit');
  } catch (error) {
    await client.query('rollback');
    throw error;
  } finally {
    client.release();
  }

  const result: ProcessResult = { processed: tasks.length, sent: 0, failed: 0, retrying: 0, discarded: 0 };
  for (const task of tasks) {
    const status = await processTask(task);
    if (status === 'sent') result.sent += 1;
    if (status === 'failed') result.failed += 1;
    if (status === 'retrying') result.retrying += 1;
    if (status === 'discarded') result.discarded += 1;
  }
  return result;
}
