import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';

import {
  ContractDocumentChange,
  diffSnapshots,
  summarizeChanges,
} from './contract-document-diff.util';
import { ContractDocumentRevision } from './entities/contract-document-revision.entity';
import type { ContractDocumentSnapshot } from './entities/contract.entity';

export interface RecordRevisionInput {
  contractId: string;
  before: ContractDocumentSnapshot | null;
  after: ContractDocumentSnapshot | null;
  actorId?: string | null;
  actorRole?: string | null;
  actorName?: string | null;
  stepId?: string | null;
}

/**
 * `iam.users.name` is a localized object ({ en, am, … }), not a string — a
 * plain `String(name)` there yields "[object Object]" in the audit trail.
 */
interface IamUserRow {
  name?: Record<string, string> | string | null;
  username?: string | null;
  email?: string | null;
}

/** Best display name for a user row: English label → any locale → login → email. */
function pickUserName(user: IamUserRow): string | null {
  const { name } = user;
  if (typeof name === 'string' && name.trim()) return name.trim();
  if (name && typeof name === 'object') {
    const localized =
      name.en ?? Object.values(name).find((v) => typeof v === 'string' && v.trim());
    if (localized?.trim()) return localized.trim();
  }
  return user.username?.trim() || user.email?.trim() || null;
}

/** Pre-computed changes (contract fields), rather than a document diff. */
export interface RecordChangesInput {
  contractId: string;
  changes: ContractDocumentChange[];
  actorId?: string | null;
  actorRole?: string | null;
  actorName?: string | null;
  stepId?: string | null;
}

@Injectable()
export class ContractDocumentHistoryService {
  private readonly logger = new Logger(ContractDocumentHistoryService.name);

  constructor(
    @InjectRepository(ContractDocumentRevision)
    private readonly revisionRepo: Repository<ContractDocumentRevision>,
    @InjectDataSource() private readonly dataSource: DataSource,
  ) {}

  /**
   * Append a revision describing what an edit changed. Best-effort: recording
   * history must never break the edit that triggered it, so failures are logged
   * and swallowed. A no-op edit records nothing.
   */
  async record(input: RecordRevisionInput): Promise<void> {
    return this.recordChanges({
      ...input,
      changes: diffSnapshots(input.before, input.after),
    });
  }

  /**
   * Append a revision from an already-computed change set — the contract-field
   * path, where there is no document snapshot to diff. Same best-effort
   * contract as {@link record}: a no-op change set records nothing, and a
   * failure here never breaks the edit that triggered it.
   */
  async recordChanges(input: RecordChangesInput): Promise<void> {
    try {
      if (input.changes.length === 0) return;

      await this.revisionRepo.save(
        this.revisionRepo.create({
          contractId: input.contractId,
          actorId: input.actorId ?? null,
          actorRole: input.actorRole ?? null,
          actorName:
            input.actorName ?? (await this.resolveActorName(input.actorId)),
          stepId: input.stepId ?? null,
          summary: summarizeChanges(input.changes),
          changes: input.changes,
        }),
      );
    } catch (err) {
      this.logger.error(
        `Failed to record document revision for contract ${input.contractId}: ${String(err)}`,
      );
    }
  }

  /**
   * Name for the acting user. `iam.users` is owned by the auth system and has
   * no entity here, so it is read directly; a miss is not an error — the trail
   * still carries the id, role and timestamp.
   */
  private async resolveActorName(
    actorId?: string | null,
  ): Promise<string | null> {
    if (!actorId) return null;
    const names = await this.resolveActorNames([actorId]);
    return names.get(actorId) ?? null;
  }

  /** Batched {@link resolveActorName} — one query for a whole revision list. */
  private async resolveActorNames(
    actorIds: string[],
  ): Promise<Map<string, string>> {
    const resolved = new Map<string, string>();
    const ids = [...new Set(actorIds.filter(Boolean))];
    if (ids.length === 0) return resolved;

    try {
      const rows = (await this.dataSource.query(
        `SELECT id, name, username, email FROM iam.users WHERE id = ANY($1::uuid[])`,
        [ids],
      )) as Array<IamUserRow & { id: string }>;
      for (const row of rows) {
        const name = pickUserName(row);
        if (name) resolved.set(row.id, name);
      }
    } catch (err) {
      this.logger.warn(`Could not resolve actor names: ${String(err)}`);
    }
    return resolved;
  }

  /** Revision history for a contract, newest first. */
  async list(contractId: string): Promise<ContractDocumentRevision[]> {
    const revisions = await this.revisionRepo.find({
      where: { contractId },
      order: { createdAt: 'DESC' },
    });

    // Rows written before actor_name existed still carry an actor_id — resolve
    // those for display (one query for the whole list) rather than backfilling.
    const missing = revisions
      .filter((r) => !r.actorName && r.actorId)
      .map((r) => r.actorId as string);
    if (missing.length === 0) return revisions;

    const names = await this.resolveActorNames(missing);
    for (const revision of revisions) {
      if (!revision.actorName && revision.actorId) {
        revision.actorName = names.get(revision.actorId) ?? null;
      }
    }
    return revisions;
  }
}
