import crypto from 'node:crypto';
import { promises as fs } from 'node:fs';
import path from 'node:path';

/**
 * File-backed cloud store for shared campaigns + member-owned characters. Sits
 * alongside AccountStore (users). A campaign has an owner (the GM) and members
 * (players who joined by invite code). Characters are owned by the player who
 * published them — only the owner updates them (last-write-wins); the GM gets
 * read access to all of a campaign's characters. Single-process; writes are
 * serialised. Swap for a real DB later without changing the wire shape.
 */

export interface CloudCampaign {
  id: string;
  ownerUserId: string;
  name: string;
  system: string;
  inviteCode: string;
  members: string[]; // userIds (excludes the owner)
  createdAt: number;
  updatedAt: number;
}

export interface CloudCharacter {
  id: string;
  campaignId: string;
  ownerUserId: string;
  name: string;
  data: string; // serialized character JSON
  updatedAt: number;
}

const INVITE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

export class CloudStore {
  private campaigns = new Map<string, CloudCampaign>();
  private characters = new Map<string, CloudCharacter>();
  private byInvite = new Map<string, string>();
  private queue: Promise<unknown> = Promise.resolve();
  private loaded = false;
  constructor(private dir: string, private now: () => number = () => Date.now()) {}

  private file() { return path.join(this.dir, 'cloud.json'); }

  async load(): Promise<void> {
    if (this.loaded) return;
    this.loaded = true;
    try {
      const raw = JSON.parse(await fs.readFile(this.file(), 'utf8')) as { campaigns: CloudCampaign[]; characters: CloudCharacter[] };
      for (const c of raw.campaigns ?? []) { this.campaigns.set(c.id, c); this.byInvite.set(c.inviteCode, c.id); }
      for (const ch of raw.characters ?? []) this.characters.set(ch.id, ch);
    } catch { /* no file yet */ }
  }

  private persist(): Promise<void> {
    this.queue = this.queue.then(async () => {
      await fs.mkdir(this.dir, { recursive: true });
      const tmp = `${this.file()}.tmp`;
      await fs.writeFile(tmp, JSON.stringify({ campaigns: [...this.campaigns.values()], characters: [...this.characters.values()] }));
      await fs.rename(tmp, this.file());
    }).catch(() => {});
    return this.queue as Promise<void>;
  }

  private mintInvite(): string {
    for (let i = 0; i < 50; i++) {
      const code = Array.from({ length: 6 }, () => INVITE_ALPHABET[crypto.randomInt(INVITE_ALPHABET.length)]).join('');
      if (!this.byInvite.has(code)) return code;
    }
    return crypto.randomBytes(4).toString('hex').toUpperCase();
  }

  async createCampaign(ownerUserId: string, name: string, system: string): Promise<CloudCampaign> {
    await this.load();
    const c: CloudCampaign = {
      id: crypto.randomUUID(), ownerUserId, name: name.slice(0, 120) || 'Campaign', system: system === 'pf2e' ? 'pf2e' : '5e',
      inviteCode: this.mintInvite(), members: [], createdAt: this.now(), updatedAt: this.now(),
    };
    this.campaigns.set(c.id, c);
    this.byInvite.set(c.inviteCode, c.id);
    await this.persist();
    return c;
  }

  /** Owner-only: invalidate the current invite code and mint a new one, so a
   *  leaked/old code can no longer be used to join. Returns the new code. */
  async rotateInvite(ownerUserId: string, campaignId: string): Promise<string | null> {
    await this.load();
    const c = this.campaigns.get(campaignId);
    if (!c || c.ownerUserId !== ownerUserId) return null;
    this.byInvite.delete(c.inviteCode);
    c.inviteCode = this.mintInvite();
    c.updatedAt = this.now();
    this.byInvite.set(c.inviteCode, c.id);
    await this.persist();
    return c.inviteCode;
  }

  /** Owner-only: remove a member and all of their published characters. */
  async removeMember(ownerUserId: string, campaignId: string, memberUserId: string): Promise<boolean> {
    await this.load();
    const c = this.campaigns.get(campaignId);
    if (!c || c.ownerUserId !== ownerUserId || memberUserId === ownerUserId) return false;
    c.members = c.members.filter((m) => m !== memberUserId);
    c.updatedAt = this.now();
    for (const [id, ch] of this.characters) if (ch.campaignId === campaignId && ch.ownerUserId === memberUserId) this.characters.delete(id);
    await this.persist();
    return true;
  }

  isMember(campaignId: string, userId: string): boolean {
    const c = this.campaigns.get(campaignId);
    return !!c && (c.ownerUserId === userId || c.members.includes(userId));
  }
  isOwner(campaignId: string, userId: string): boolean {
    return this.campaigns.get(campaignId)?.ownerUserId === userId;
  }

  async listForUser(userId: string): Promise<Array<CloudCampaign & { role: 'owner' | 'member' }>> {
    await this.load();
    const out: Array<CloudCampaign & { role: 'owner' | 'member' }> = [];
    for (const c of this.campaigns.values()) {
      if (c.ownerUserId === userId) out.push({ ...c, role: 'owner' });
      else if (c.members.includes(userId)) out.push({ ...c, role: 'member' });
    }
    return out;
  }

  async joinByInvite(userId: string, inviteCode: string): Promise<CloudCampaign | null> {
    await this.load();
    const id = this.byInvite.get(inviteCode.trim().toUpperCase());
    const c = id ? this.campaigns.get(id) : undefined;
    if (!c) return null;
    if (c.ownerUserId !== userId && !c.members.includes(userId)) { c.members.push(userId); c.updatedAt = this.now(); await this.persist(); }
    return c;
  }

  /** Upsert a character the user owns. Returns null if they're not a member or don't own an existing record. */
  async putCharacter(userId: string, campaignId: string, char: { id: string; name: string; data: string }): Promise<CloudCharacter | null> {
    await this.load();
    if (!this.isMember(campaignId, userId)) return null;
    const existing = this.characters.get(char.id);
    if (existing && existing.ownerUserId !== userId) return null; // only the owner may update
    const record: CloudCharacter = { id: char.id, campaignId, ownerUserId: existing?.ownerUserId ?? userId, name: char.name.slice(0, 120), data: char.data, updatedAt: this.now() };
    this.characters.set(record.id, record);
    await this.persist();
    return record;
  }

  async listCharacters(campaignId: string): Promise<CloudCharacter[]> {
    await this.load();
    return [...this.characters.values()].filter((c) => c.campaignId === campaignId).sort((a, b) => a.name.localeCompare(b.name));
  }

  async removeCharacter(userId: string, characterId: string): Promise<boolean> {
    await this.load();
    const ch = this.characters.get(characterId);
    if (!ch) return false;
    if (ch.ownerUserId !== userId && !this.isOwner(ch.campaignId, userId)) return false; // owner of char or campaign owner
    this.characters.delete(characterId);
    await this.persist();
    return true;
  }

  /** Total bytes a user is responsible for (their characters' data) — for usage tracking. */
  async usageBytes(userId: string): Promise<number> {
    await this.load();
    let bytes = 0;
    for (const c of this.characters.values()) if (c.ownerUserId === userId) bytes += Buffer.byteLength(c.data);
    return bytes;
  }
}
