import crypto from 'node:crypto';
import { pool } from '../db.js';
import { config } from '../config.js';
import {
  defaultSupervisionDueAt,
  generateStudentSupervisionFollowups,
  nextSupervisionRunAt,
  parseSupervisionRules,
  parseSupervisionSchedule,
  shanghaiTodayKey,
  supervisionSystemAuth,
} from '../../../api/src/features/tenant-admin/supervision.js';

interface SupervisionRuleCandidate {
  id: string;
  tenantId: string;
  name: string;
  rules: Record<string, unknown>;
  schedule: Record<string, unknown>;
  classId: string | null;
  assignedToUserId: string | null;
  limit: number;
  metadata: Record<string, unknown>;
}

export interface StudentSupervisionWorkerResult {
  processed: number;
  generated: number;
  failed: number;
  skipped: number;
  followups: number;
}

function nowIso() {
  return new Date().toISOString();
}

function errorMessage(error: unknown) {
  return error instanceof Error ? error.message : String(error);
}

function errorCode(error: unknown) {
  return typeof error === 'object' && error !== null && 'code' in error
    ? String((error as { code?: unknown }).code || 'STUDENT_SUPERVISION_WORKER_ERROR')
    : 'STUDENT_SUPERVISION_WORKER_ERROR';
}

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

function stableBatchKey(rule: SupervisionRuleCandidate) {
  const raw = [
    'worker',
    rule.id,
    shanghaiTodayKey(),
  ].join(':');
  return raw.length <= 120 ? raw : crypto.createHash('sha256').update(raw).digest('hex');
}

async function claimStudentSupervisionRules(limit: number, claimId: string) {
  const client = await pool.connect();
  try {
    await client.query('begin');
    const result = await client.query<SupervisionRuleCandidate>(
      `
      with candidates as (
        select sr.id
        from public.tenant_student_supervision_rules sr
        join public.tenants t on t.id = sr.tenant_id
        where t.status = 'active'
          and sr.status = 'active'
          and coalesce(sr.schedule->>'enabled', 'false') = 'true'
          and coalesce(sr.schedule->>'frequency', 'manual') <> 'manual'
          and coalesce(sr.next_run_at, sr.created_at) <= now()
          and coalesce(nullif(sr.metadata #>> '{studentSupervisionWorker,claimedAt}', '')::timestamptz, '1970-01-01'::timestamptz)
            <= now() - ($2::integer * interval '1 second')
        order by coalesce(sr.next_run_at, sr.created_at) asc, sr.created_at asc
        limit $1
        for update of sr skip locked
      )
      update public.tenant_student_supervision_rules sr
      set metadata = jsonb_set(
            coalesce(sr.metadata, '{}'::jsonb),
            '{studentSupervisionWorker}',
            coalesce(sr.metadata->'studentSupervisionWorker', '{}'::jsonb) || $3::jsonb,
            true
          ),
          updated_at = now()
      from candidates
      where sr.id = candidates.id
      returning sr.id,
                sr.tenant_id as "tenantId",
                sr.name,
                sr.rules,
                sr.schedule,
                sr.class_id as "classId",
                sr.assigned_to_user_id as "assignedToUserId",
                sr.limit_count as "limit",
                sr.metadata
      `,
      [
        Math.max(1, Math.min(100, limit)),
        Math.max(60, config.studentSupervisionClaimStaleSeconds),
        JSON.stringify({
          claimId,
          workerId: config.studentSupervisionWorkerId,
          claimedAt: nowIso(),
        }),
      ],
    );
    await client.query('commit');
    return result.rows;
  } catch (error) {
    await client.query('rollback');
    throw error;
  } finally {
    client.release();
  }
}

async function markRuleFailed(rule: SupervisionRuleCandidate, error: unknown) {
  const schedule = parseSupervisionSchedule(rule.schedule);
  const nextRunAt = nextSupervisionRunAt(schedule);
  const details = {
    code: errorCode(error),
    message: truncate(errorMessage(error)),
    failedAt: nowIso(),
    workerId: config.studentSupervisionWorkerId,
  };
  await pool.query(
    `
    update public.tenant_student_supervision_rules
    set last_run_at = now(),
        next_run_at = $3::timestamptz,
        last_result = $4::jsonb,
        metadata = jsonb_set(
          coalesce(metadata, '{}'::jsonb),
          '{studentSupervisionWorker}',
          coalesce(metadata->'studentSupervisionWorker', '{}'::jsonb) || $5::jsonb,
          true
        ),
        updated_at = now()
    where tenant_id = $1 and id = $2
    `,
    [
      rule.tenantId,
      rule.id,
      nextRunAt,
      JSON.stringify({ status: 'failed', ...details }),
      JSON.stringify({
        lastStatus: 'failed',
        lastError: details,
        lastWorkerId: config.studentSupervisionWorkerId,
        lastFinishedAt: nowIso(),
      }),
    ],
  );
  await pool.query(
    `
    insert into public.audit_logs (tenant_id, actor_user_id, action, target_type, target_id, details)
    values ($1, null, 'tenant.students.supervision_rule_worker_failed', 'tenant_student_supervision_rules', $2, $3::jsonb)
    `,
    [rule.tenantId, rule.id, JSON.stringify(details)],
  ).catch(() => {});
}

async function processRule(rule: SupervisionRuleCandidate) {
  const rules = parseSupervisionRules(rule.rules);
  const schedule = parseSupervisionSchedule(rule.schedule);
  const result = await generateStudentSupervisionFollowups(
    supervisionSystemAuth(rule.tenantId),
    {
      rules,
      classId: rule.classId,
      assignedToUserId: rule.assignedToUserId,
      dueAt: defaultSupervisionDueAt(),
      batchKey: stableBatchKey(rule),
      limit: Math.max(1, Math.min(100, Number(rule.limit || 20))),
      metadata: {
        ...rule.metadata,
        source: 'student_supervision_worker',
        ruleId: rule.id,
        ruleName: rule.name,
        workerId: config.studentSupervisionWorkerId,
      },
    },
  );
  const nextRunAt = nextSupervisionRunAt(schedule);
  await pool.query(
    `
    update public.tenant_student_supervision_rules
    set last_run_at = now(),
        next_run_at = $3::timestamptz,
        last_result = $4::jsonb,
        metadata = jsonb_set(
          coalesce(metadata, '{}'::jsonb),
          '{studentSupervisionWorker}',
          coalesce(metadata->'studentSupervisionWorker', '{}'::jsonb) || $5::jsonb,
          true
        ),
        updated_at = now()
    where tenant_id = $1 and id = $2
    `,
    [
      rule.tenantId,
      rule.id,
      nextRunAt,
      JSON.stringify({
        status: result.errorCount ? 'completed_with_errors' : 'completed',
        ranAt: nowIso(),
        nextRunAt,
        batchKey: result.batchKey,
        total: result.total,
        successCount: result.successCount,
        errorCount: result.errorCount,
      }),
      JSON.stringify({
        lastStatus: result.errorCount ? 'completed_with_errors' : 'completed',
        lastWorkerId: config.studentSupervisionWorkerId,
        lastFinishedAt: nowIso(),
      }),
    ],
  );
  await pool.query(
    `
    insert into public.audit_logs (tenant_id, actor_user_id, action, target_type, target_id, details)
    values ($1, null, 'tenant.students.supervision_rule_worker_completed', 'tenant_student_supervision_rules', $2, $3::jsonb)
    `,
    [
      rule.tenantId,
      rule.id,
      JSON.stringify({
        batchKey: result.batchKey,
        total: result.total,
        successCount: result.successCount,
        errorCount: result.errorCount,
        workerId: config.studentSupervisionWorkerId,
      }),
    ],
  );
  return result;
}

export async function processStudentSupervisionBatch(): Promise<StudentSupervisionWorkerResult> {
  const claimId = crypto.randomUUID();
  const rules = await claimStudentSupervisionRules(config.studentSupervisionBatchSize, claimId);
  const result: StudentSupervisionWorkerResult = {
    processed: 0,
    generated: 0,
    failed: 0,
    skipped: 0,
    followups: 0,
  };

  for (const rule of rules) {
    result.processed += 1;
    try {
      const generated = await processRule(rule);
      result.generated += 1;
      result.followups += generated.successCount || 0;
    } catch (error) {
      result.failed += 1;
      await markRuleFailed(rule, error);
    }
  }

  return result;
}
