import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname } from "path";
import {
  AddressTrackingStore,
  TrackedAddressRecord,
  WebhookShardMetadata,
} from "../types";
import { IRegisteredAddressPort } from "../providers/ProviderPorts";
import { ITrackingStore } from "./StoreContracts";

export class AlchemyTrackingStoreService implements ITrackingStore {
  constructor(private filePath: string) {
    this.ensureStore();
  }

  async getAllTrackedAddresses(): Promise<Array<{ chainType: string; address: string }>> {
    return (await this.getAllShards()).flatMap((shard) =>
      shard.addresses.map((address) => ({ chainType: shard.chain_type, address }))
    );
  }

  async getAllShards(): Promise<WebhookShardMetadata[]> {
    const store = this.readStore();
    return Object.values(store.webhooks).sort((left, right) => left.shard_key.localeCompare(right.shard_key));
  }

  async getShardByWebhookId(webhookId: string): Promise<WebhookShardMetadata | null> {
    const store = this.readStore();
    return Object.values(store.webhooks).find((webhook) => webhook.webhook_id === webhookId) || null;
  }

  async getAddress(address: string): Promise<TrackedAddressRecord | null> {
    const store = this.readStore();
    return store.addresses[this.normalizeAddress(address)] || null;
  }

  async getShardsByChainType(chainType: string): Promise<WebhookShardMetadata[]> {
    return (await this.getAllShards())
      .filter((webhook) => webhook.chain_type === chainType)
      .sort((left, right) => {
        if (left.address_count !== right.address_count) {
          return left.address_count - right.address_count;
        }

        return left.shard_key.localeCompare(right.shard_key);
      });
  }

  async upsertShards(shards: WebhookShardMetadata[]): Promise<void> {
    const store = this.readStore();
    for (const shard of shards) {
      const existing = store.webhooks[shard.shard_key];
      store.webhooks[shard.shard_key] = existing
        ? {
            ...existing,
            ...shard,
            addresses: existing.addresses,
            address_count: existing.address_count,
          }
        : shard;
    }

    this.writeStore(store);
  }

  async registerAssignments(assignments: TrackedAddressRecord[]): Promise<void> {
    if (assignments.length === 0) {
      return;
    }

    const store = this.readStore();
    for (const assignment of assignments) {
      const normalizedAddress = assignment.normalized_address;
      const existing = store.addresses[normalizedAddress];
      if (existing?.shard_key && existing.shard_key !== assignment.shard_key) {
        const previousShard = store.webhooks[existing.shard_key];
        if (previousShard) {
          previousShard.addresses = previousShard.addresses.filter((item) => item !== normalizedAddress);
          previousShard.address_count = previousShard.addresses.length;
        }
      }

      store.addresses[normalizedAddress] = assignment;

      const shard = store.webhooks[assignment.shard_key];
      if (!shard) {
        throw new Error(`Unknown tracking shard '${assignment.shard_key}'`);
      }

      if (!shard.addresses.includes(normalizedAddress)) {
        shard.addresses.push(normalizedAddress);
        shard.addresses.sort();
      }
      shard.address_count = shard.addresses.length;
      shard.last_synced_at = assignment.updated_at;
    }

    this.writeStore(store);
  }

  async unregisterAddresses(addresses: string[]): Promise<void> {
    if (addresses.length === 0) {
      return;
    }

    const store = this.readStore();

    for (const address of addresses) {
      const normalizedAddress = this.normalizeAddress(address);
      const existing = store.addresses[normalizedAddress];

      if (!existing) {
        continue;
      }

      const shard = store.webhooks[existing.shard_key];
      if (shard) {
        shard.addresses = shard.addresses.filter((item) => item !== normalizedAddress);
        shard.address_count = shard.addresses.length;
        shard.last_synced_at = new Date().toISOString();
      }

      delete store.addresses[normalizedAddress];
    }

    this.writeStore(store);
  }

  private ensureStore(): void {
    if (!existsSync(dirname(this.filePath))) {
      mkdirSync(dirname(this.filePath), { recursive: true });
    }

    if (!existsSync(this.filePath)) {
      this.writeStore({
        version: 1,
        addresses: {},
        webhooks: {},
      });
    }
  }

  private readStore(): AddressTrackingStore {
    const store = JSON.parse(readFileSync(this.filePath, "utf8")) as Partial<AddressTrackingStore>;
    return {
      version: 1,
      addresses: store.addresses || {},
      webhooks: store.webhooks || {},
    };
  }

  private writeStore(store: AddressTrackingStore): void {
    writeFileSync(this.filePath, JSON.stringify(store, null, 2), "utf8");
  }

  private normalizeAddress(address: string): string {
    const trimmed = address.trim();
    return trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.toLowerCase() : trimmed;
  }
}
