import { randomUUID } from 'crypto';
import type {
  AuditPublisher,
  CustodyAdapter,
  MintJobSink,
  OutstandingPolicy,
} from './custody-adapter';
import type {
  AuditRecord,
  BasketMandateSnapshot,
  BitcoinDepositEvent,
  DepositInstruction,
  MintJob,
} from './types';

const DEFAULT_CONFIRMATIONS_REQUIRED = 6;

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

function clone<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T;
}

export class NativeBitcoinWatcher {
  private readonly instructions = new Map<string, DepositInstruction>();
  private readonly mintJobs = new Map<string, MintJob>();
  private readonly auditRecords: AuditRecord[] = [];
  private cursor?: string;

  constructor(
    private readonly adapter: CustodyAdapter,
    private readonly mintSink: MintJobSink,
    private readonly auditPublisher: AuditPublisher,
    private readonly outstandingPolicy?: OutstandingPolicy,
  ) {}

  async createDepositInstruction(input: {
    basketMandate: BasketMandateSnapshot;
    expectedAmountSats?: number;
  }): Promise<DepositInstruction> {
    const instructionId = randomUUID();
    const depositAddress = await this.adapter.allocateDepositAddress({
      instructionId,
      clientId: input.basketMandate.clientId,
    });
    const timestamp = nowIso();

    const instruction: DepositInstruction = {
      id: instructionId,
      basketMandate: clone(input.basketMandate),
      depositAddress,
      expectedAmountSats: input.expectedAmountSats,
      currentConfirmations: 0,
      confirmationsRequired: DEFAULT_CONFIRMATIONS_REQUIRED,
      status: 'instruction_created',
      createdAt: timestamp,
      updatedAt: timestamp,
    };

    this.instructions.set(instruction.id, instruction);
    await this.publishAudit({
      id: randomUUID(),
      instructionId,
      category: 'deposit_instruction',
      message: 'Allocated BTC deposit address for Chain 138 cBTC mint flow',
      metadata: {
        depositAddress,
        basketMandateId: input.basketMandate.id,
      },
      createdAt: timestamp,
    });

    return clone(instruction);
  }

  getInstruction(id: string): DepositInstruction | null {
    const instruction = this.instructions.get(id);
    return instruction ? clone(instruction) : null;
  }

  getAuditRecords(): AuditRecord[] {
    return clone(this.auditRecords);
  }

  getMintJobs(): MintJob[] {
    return Array.from(this.mintJobs.values()).map((job) => clone(job));
  }

  async sync(): Promise<DepositInstruction[]> {
    const { events, cursor } = await this.adapter.listDepositEvents(this.cursor);
    for (const event of events) {
      await this.applyEvent(event);
    }
    this.cursor = cursor;
    return Array.from(this.instructions.values()).map((instruction) => clone(instruction));
  }

  private async applyEvent(event: BitcoinDepositEvent): Promise<void> {
    const instruction = this.instructions.get(event.instructionId);
    if (!instruction) {
      return;
    }

    if (
      instruction.observedTxId === event.txId &&
      instruction.currentConfirmations >= event.confirmations &&
      instruction.status !== 'frozen'
    ) {
      return;
    }

    instruction.observedTxId = event.txId;
    instruction.currentConfirmations = Math.max(0, event.confirmations);
    instruction.expectedAmountSats = event.amountSats;
    instruction.updatedAt = event.observedAt || nowIso();

    if (event.type === 'reorg' || event.confirmations < instruction.confirmationsRequired) {
      instruction.status = 'pending_confirmations';
      await this.publishAudit({
        id: randomUUID(),
        instructionId: instruction.id,
        category: 'confirmation',
        message: 'Deposit observed but waiting for final BTC confirmations',
        metadata: {
          txId: event.txId,
          confirmations: event.confirmations,
          type: event.type,
        },
        createdAt: instruction.updatedAt,
      });
      return;
    }

    instruction.status = 'confirmed';
    await this.publishAudit({
      id: randomUUID(),
      instructionId: instruction.id,
      category: 'confirmation',
      message: 'Deposit reached 6 confirmations and is eligible for minting',
      metadata: {
        txId: event.txId,
        confirmations: event.confirmations,
        amountSats: event.amountSats,
      },
      createdAt: instruction.updatedAt,
    });

    await this.maybeQueueMint(instruction, event.amountSats);
  }

  private async maybeQueueMint(instruction: DepositInstruction, amountSats: number): Promise<void> {
    if (this.mintJobs.has(instruction.id)) {
      instruction.status = 'minted';
      return;
    }

    const reserveBalanceSats = await this.adapter.getConfirmedReserveBalanceSats();
    const alreadyMintedSats = Array.from(this.mintJobs.values()).reduce(
      (total, job) => total + job.amountSats,
      0,
    );

    if (reserveBalanceSats < alreadyMintedSats + amountSats) {
      await this.freezeInstruction(
        instruction,
        'Reserve deficit detected during cBTC reconciliation',
        {
          reserveBalanceSats,
          alreadyMintedSats,
          requestedMintSats: amountSats,
        },
      );
      return;
    }

    if (this.outstandingPolicy) {
      const currentOutstandingSats = await this.outstandingPolicy.getCurrentOutstandingSats();
      const maxOutstandingSats = await this.outstandingPolicy.getMaxOutstandingSats();

      if (maxOutstandingSats > 0 && currentOutstandingSats + amountSats > maxOutstandingSats) {
        await this.freezeInstruction(
          instruction,
          'Bridge outstanding limit would be exceeded by mint request',
          {
            currentOutstandingSats,
            maxOutstandingSats,
            requestedMintSats: amountSats,
          },
        );
        return;
      }
    }

    const timestamp = nowIso();
    const mintJob: MintJob = {
      id: randomUUID(),
      instructionId: instruction.id,
      basketMandateId: instruction.basketMandate.id,
      chain138VaultAddress: instruction.basketMandate.chain138VaultAddress,
      canonicalSymbol: 'cBTC',
      amountSats,
      status: 'queued',
      createdAt: timestamp,
    };

    await this.mintSink.enqueue(mintJob);
    this.mintJobs.set(instruction.id, mintJob);
    instruction.status = 'minted';
    instruction.updatedAt = timestamp;

    await this.publishAudit({
      id: randomUUID(),
      instructionId: instruction.id,
      category: 'mint_job',
      message: 'Queued auto-mint job for confirmed BTC deposit',
      metadata: {
        mintJobId: mintJob.id,
        chain138VaultAddress: mintJob.chain138VaultAddress,
        amountSats,
      },
      createdAt: timestamp,
    });
  }

  private async freezeInstruction(
    instruction: DepositInstruction,
    reason: string,
    metadata?: Record<string, unknown>,
  ): Promise<void> {
    instruction.status = 'frozen';
    instruction.freezeReason = reason;
    instruction.updatedAt = nowIso();

    await this.publishAudit({
      id: randomUUID(),
      instructionId: instruction.id,
      category: 'freeze',
      message: reason,
      metadata,
      createdAt: instruction.updatedAt,
    });
  }

  private async publishAudit(record: AuditRecord): Promise<void> {
    this.auditRecords.push(record);
    await this.auditPublisher.publish(record);
  }
}
