import { appendFile, chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import type {
  RingCentralCallMetadata,
  VoiceCallAuditRecord,
  VoiceCallReconciliation,
} from "./types.ts";

export class JsonlVoiceAuditStore {
  constructor(private readonly path: string) {}

  async append(record: VoiceCallAuditRecord): Promise<void> {
    await ensurePrivateDirectory(dirname(this.path));
    await appendFile(this.path, `${JSON.stringify(record)}\n`, { mode: 0o600 });
    await chmod(this.path, 0o600);
  }

  async readAll(): Promise<VoiceCallAuditRecord[]> {
    try {
      const text = await readFile(this.path, "utf8");
      return text
        .split("\n")
        .filter((line) => line.trim())
        .map((line) => JSON.parse(line) as VoiceCallAuditRecord);
    } catch (err) {
      if ((err as { code?: unknown }).code === "ENOENT") return [];
      throw err;
    }
  }
}

export interface RingCentralCallLogPuller {
  callTool(name: string, args?: Record<string, unknown>): Promise<unknown>;
}

export interface AuditReconciliationResult {
  total: number;
  matched: number;
  outputPath: string;
}

export function defaultAuditLogPath(): string {
  const home = homedir();
  if (!home || home === "/") return "/tmp/exult-voice-agent-audit.jsonl";
  return join(home, ".local", "state", "exult-voice-agent", "audit.jsonl");
}

export async function reconcileAuditLog(input: {
  auditPath: string;
  outputPath: string;
  ringCentral: RingCentralCallLogPuller;
  dateFrom: string;
  dateTo: string;
}): Promise<AuditReconciliationResult> {
  const store = new JsonlVoiceAuditStore(input.auditPath);
  const records = await store.readAll();
  const callLog = (await input.ringCentral.callTool("pull_call_log", {
    date_from: input.dateFrom,
    date_to: input.dateTo,
    max_pages: 3,
  })) as { records?: unknown[] };
  const callLogRecords = callLog.records ?? [];

  const reconciled = records.map((record) => {
    const reconciliation = reconcileRecord(record, callLogRecords);
    return {
      ...record,
      reconciliation,
      ringCentral: {
        ...record.ringCentral,
        ...ringCentralMetadataFromReconciliation(reconciliation),
      },
    };
  });

  await ensurePrivateDirectory(dirname(input.outputPath));
  const tmpPath = `${input.outputPath}.tmp`;
  await writeFile(tmpPath, reconciled.map((record) => JSON.stringify(record)).join("\n") + "\n", {
    mode: 0o600,
  });
  await chmod(tmpPath, 0o600);
  await rename(tmpPath, input.outputPath);
  await chmod(input.outputPath, 0o600);
  return {
    total: records.length,
    matched: reconciled.filter((record) => record.reconciliation?.matched).length,
    outputPath: input.outputPath,
  };
}

export function reconcileRecord(
  audit: VoiceCallAuditRecord,
  callLogRecords: unknown[],
): VoiceCallReconciliation {
  const identifiers = identifiersForAudit(audit);
  for (const callLogRecord of callLogRecords) {
    for (const identifier of identifiers) {
      const match = findObjectContainingString(callLogRecord, identifier.value);
      if (match) {
        return {
          reconciledAt: new Date().toISOString(),
          matched: true,
          matchedBy: identifier.kind,
          ...reconciliationMetadata(match),
        };
      }
    }
  }
  return { reconciledAt: new Date().toISOString(), matched: false };
}

function identifiersForAudit(
  audit: VoiceCallAuditRecord,
): Array<{ kind: "telephonySessionId" | "sessionId" | "callId"; value: string }> {
  return [
    audit.ringCentral.telephonySessionId
      ? { kind: "telephonySessionId" as const, value: audit.ringCentral.telephonySessionId }
      : undefined,
    audit.ringCentral.sessionId
      ? { kind: "sessionId" as const, value: audit.ringCentral.sessionId }
      : undefined,
    audit.callId ? { kind: "callId" as const, value: audit.callId } : undefined,
  ].filter((value): value is { kind: "telephonySessionId" | "sessionId" | "callId"; value: string } =>
    Boolean(value),
  );
}

function reconciliationMetadata(value: unknown): Omit<VoiceCallReconciliation, "reconciledAt" | "matched" | "matchedBy"> {
  const object = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
  const nested = collectFirstStringFields(object, new Set(["id", "sessionId", "telephonySessionId", "contentUri"]));
  return {
    callLogId: stringValue(object.id) ?? nested.id,
    sessionId: stringValue(object.sessionId) ?? nested.sessionId,
    telephonySessionId: stringValue(object.telephonySessionId) ?? nested.telephonySessionId,
    recordingContentUri: findRecordingContentUri(object) ?? nested.contentUri,
  };
}

function ringCentralMetadataFromReconciliation(
  reconciliation: VoiceCallReconciliation,
): Partial<RingCentralCallMetadata> {
  return {
    callLogId: reconciliation.callLogId,
    sessionId: reconciliation.sessionId,
    telephonySessionId: reconciliation.telephonySessionId,
    recordingContentUri: reconciliation.recordingContentUri,
  };
}

function findObjectContainingString(value: unknown, needle: string): Record<string, unknown> | undefined {
  if (typeof value === "string") {
    return undefined;
  }
  if (Array.isArray(value)) {
    for (const item of value) {
      const match = findObjectContainingString(item, needle);
      if (match) return match;
    }
    return undefined;
  }
  if (value && typeof value === "object") {
    const object = value as Record<string, unknown>;
    for (const item of Object.values(object)) {
      if (item === needle) return object;
      const match = findObjectContainingString(item, needle);
      if (match) return match;
    }
  }
  return undefined;
}

function collectFirstStringFields(
  value: unknown,
  fieldNames: Set<string>,
  output: Record<string, string> = {},
): Record<string, string> {
  if (Array.isArray(value)) {
    for (const item of value) collectFirstStringFields(item, fieldNames, output);
    return output;
  }
  if (!value || typeof value !== "object") return output;

  for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
    if (fieldNames.has(key) && output[key] === undefined) {
      const string = stringValue(item);
      if (string) output[key] = string;
    }
    collectFirstStringFields(item, fieldNames, output);
  }
  return output;
}

function findRecordingContentUri(value: unknown): string | undefined {
  if (Array.isArray(value)) {
    for (const item of value) {
      const uri = findRecordingContentUri(item);
      if (uri) return uri;
    }
    return undefined;
  }
  if (!value || typeof value !== "object") return undefined;

  const object = value as Record<string, unknown>;
  const recording = object.recording;
  if (recording && typeof recording === "object") {
    const uri = stringValue((recording as Record<string, unknown>).contentUri);
    if (uri) return uri;
  }

  for (const item of Object.values(object)) {
    const uri = findRecordingContentUri(item);
    if (uri) return uri;
  }
  return undefined;
}

function stringValue(value: unknown): string | undefined {
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
}

async function ensurePrivateDirectory(path: string): Promise<void> {
  await mkdir(path, { recursive: true, mode: 0o700 });
  await chmod(path, 0o700);
}
