// Domain-specific helpers on top of lib/db.js.
// Keeps callers (popup, background, content) free of IndexedDB details.

import { put, get, getAll, getByIndex, remove, STORES, uuid } from './db.js';

// ---------- Campaigns ----------
// { id, name, status, groupIds[], rotation, settings: { ... }, startedAt, pausedAt }
// status: 'draft' | 'active' | 'paused' | 'done'
// rotation: 'random' | 'lru' | 'weighted' | 'roundrobin'

export const Campaigns = {
  create(fields = {}) {
    return put(STORES.campaigns, {
      id: uuid(),
      name: fields.name || 'קמפיין חדש',
      status: 'draft',
      groupIds: fields.groupIds || [],
      rotation: fields.rotation || 'lru',  // least-recently-used = best variety
      settings: fields.settings || {},
      startedAt: null,
      pausedAt: null,
    });
  },
  update(campaign, fields) { return put(STORES.campaigns, { ...campaign, ...fields }); },
  list() { return getAll(STORES.campaigns); },
  listActive() { return getByIndex(STORES.campaigns, 'status', 'active'); },
  get(id) { return get(STORES.campaigns, id); },
  remove(id) { return remove(STORES.campaigns, id); },
};

// ---------- Groups ----------
// A group record:
// { id, fbGroupId, name, url, memberCount, isMember, source, lastPostedAt, notes }
// source: 'joined' (scraped from my groups) | 'discovered' (search) | 'manual'

export const Groups = {
  upsertByFbId(fbGroupId, fields) {
    return getByIndex(STORES.groups, 'fbGroupId', fbGroupId).then((existing) => {
      const record = existing[0]
        ? { ...existing[0], ...fields, fbGroupId }
        : { id: uuid(), fbGroupId, isMember: false, source: 'manual', ...fields };
      return put(STORES.groups, record);
    });
  },
  list() { return getAll(STORES.groups); },
  listMembers() { return getByIndex(STORES.groups, 'isMember', 1); },
  get(id) { return get(STORES.groups, id); },
  remove(id) { return remove(STORES.groups, id); },
};

// ---------- Posts (templates) ----------
// { id, campaignId, title, text, mediaUrls[], tags[], weight, lastUsedAt, useCount }

export const Posts = {
  save(post) {
    return put(STORES.posts, {
      weight: 1,
      useCount: 0,
      lastUsedAt: null,
      ...post,
    });
  },
  list() { return getAll(STORES.posts); },
  listByCampaign(campaignId) { return getByIndex(STORES.posts, 'campaignId', campaignId); },
  get(id) { return get(STORES.posts, id); },
  remove(id) { return remove(STORES.posts, id); },

  // Pick a post for the next publish according to the campaign's rotation policy.
  // The `group` arg is optional — used by 'smart-ai' to make context-aware picks.
  async pickForCampaign(campaign, group = null) {
    const posts = await Posts.listByCampaign(campaign.id);
    if (!posts.length) return null;

    // smart-ai: ask Gemini to pick the best post for this specific group.
    // Falls back to LRU if Gemini is unavailable.
    if (campaign.rotation === 'smart-ai' && group) {
      try {
        const { geminiPickBestPost } = await import('./gemini.js');
        const r = await geminiPickBestPost(posts, group);
        if (r.ok) {
          const picked = posts.find((p) => p.id === r.postId);
          if (picked) return picked;
        }
      } catch {}
      // Fallthrough to LRU
    }

    switch (campaign.rotation) {
      case 'random': {
        return posts[Math.floor(Math.random() * posts.length)];
      }

      case 'lru': {
        // Least-recently-used: prefer posts that haven't been used yet,
        // then those used longest ago. Best variety.
        const never = posts.filter((p) => !p.lastUsedAt);
        if (never.length) return never[Math.floor(Math.random() * never.length)];
        return posts.slice().sort((a, b) => (a.lastUsedAt || 0) - (b.lastUsedAt || 0))[0];
      }

      case 'weighted': {
        // Higher `weight` -> picked more often. Default weight is 1.
        const total = posts.reduce((s, p) => s + (p.weight || 1), 0);
        let r = Math.random() * total;
        for (const p of posts) {
          r -= (p.weight || 1);
          if (r <= 0) return p;
        }
        return posts[posts.length - 1];
      }

      case 'roundrobin':
      default: {
        // Sort by useCount asc, then by lastUsedAt asc — strict rotation.
        return posts.slice().sort((a, b) => {
          const c = (a.useCount || 0) - (b.useCount || 0);
          return c !== 0 ? c : (a.lastUsedAt || 0) - (b.lastUsedAt || 0);
        })[0];
      }
    }
  },

  async markUsed(post) {
    return Posts.save({
      ...post,
      lastUsedAt: Date.now(),
      useCount: (post.useCount || 0) + 1,
    });
  },
};

// ---------- Jobs (publish queue) ----------
// { id, campaignId, groupId, status, scheduledAt, attempts, lastError,
//   chosenPostId, completedAt }
// status: 'queued' | 'running' | 'done' | 'failed' | 'cancelled'
// chosenPostId: filled in when the job actually runs (rotation picks then).

export const Jobs = {
  // `extra` lets callers pre-assign fields — e.g. the AI-Mode schedule consumer
  // passes { aiScheduleId, chosenPostId } so runJob publishes that exact post
  // and tickInner reports the outcome back to the server. (ADR-31)
  enqueue(campaignId, groupId, scheduledAt, extra = {}) {
    return put(STORES.jobs, {
      id: uuid(),
      campaignId,
      groupId,
      status: 'queued',
      scheduledAt: scheduledAt || Date.now(),
      attempts: 0,
      lastError: null,
      chosenPostId: null,
      completedAt: null,
      ...extra,
    });
  },
  update(job, fields) { return put(STORES.jobs, { ...job, ...fields }); },
  list() { return getAll(STORES.jobs); },
  listByStatus(status) { return getByIndex(STORES.jobs, 'status', status); },
  listByCampaign(campaignId) { return getByIndex(STORES.jobs, 'campaignId', campaignId); },
  get(id) { return get(STORES.jobs, id); },
  remove(id) { return remove(STORES.jobs, id); },
};

// ---------- Logs ----------
// { id, jobId, level, message, meta }
// level: 'info' | 'warn' | 'error'

export const Logs = {
  add(level, message, meta = {}, jobId = null) {
    const p = put(STORES.logs, { id: uuid(), level, message, meta, jobId });
    // Live activity push — every automation step already calls Logs.add, so an
    // open popup gets a real-time feed of what the agent is doing ("not in the
    // dark"). Fire-and-forget; ignored if no popup is listening.
    try { const _r = chrome?.runtime?.sendMessage?.({ type: 'AGENT_ACTIVITY', entry: { level, message, at: Date.now() } }); if (_r && _r.catch) _r.catch(() => {}); } catch {}
    return p;
  },
  list(limit = 200) {
    return getAll(STORES.logs).then((rows) =>
      rows.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit));
  },
};

// ---------- Media (blobs attached to posts) ----------
// { id, postId, blob, mimeType, sizeBytes, r2Key, syncStatus }
//   blob:      Blob (stored locally in IndexedDB)
//   r2Key:     populated once uploaded to R2
//   syncStatus: 'pending' (needs upload) | 'synced' (already on R2)

export const Media = {
  async add({ postId, blob, mimeType }) {
    return put(STORES.media, {
      id: uuid(),
      postId,
      blob,
      mimeType: mimeType || blob.type,
      sizeBytes: blob.size,
      r2Key: null,
    });
  },
  listByPost(postId) { return getByIndex(STORES.media, 'postId', postId); },
  get(id) { return get(STORES.media, id); },
  remove(id) { return remove(STORES.media, id); },
  pendingUploads() { return getByIndex(STORES.media, 'syncStatus', 'pending'); },
  async markUploaded(id, r2Key) {
    const m = await get(STORES.media, id);
    if (!m) return;
    return put(STORES.media, { ...m, r2Key, syncStatus: 'synced' });
  },
};

// ---------- Comments queue ----------
// kind: 'bump' | 'reply' | 'manual' | 'engagement'
// status: 'queued' | 'posted' | 'failed' | 'cancelled'

export const Comments = {
  enqueue({ parentPostUrl, parentJobId, kind, text, scheduledAt, aiGenerated, approvedByUser }) {
    return put(STORES.comments, {
      id: uuid(),
      parentPostUrl, parentJobId: parentJobId || null,
      kind: kind || 'manual',
      text,
      status: 'queued',
      scheduledAt: scheduledAt || Date.now(),
      postedAt: null,
      lastError: null,
      aiGenerated: !!aiGenerated,
      approvedByUser: approvedByUser !== false ? 1 : 0,
    });
  },
  update(c, fields) { return put(STORES.comments, { ...c, ...fields }); },
  list() { return getAll(STORES.comments); },
  listByStatus(status) { return getByIndex(STORES.comments, 'status', status); },
  get(id) { return get(STORES.comments, id); },
  remove(id) { return remove(STORES.comments, id); },
};

// ---------- Discoveries (cached group search results) ----------
// { id, query, results, autoTrack, lastRunAt }
//   autoTrack: if true, the discovery alarm re-runs this query daily.

export const Discoveries = {
  save(query, results, opts = {}) {
    return put(STORES.discoveries, {
      id: uuid(),
      query,
      results,
      autoTrack: !!opts.autoTrack,
      lastRunAt: Date.now(),
    });
  },
  // Save or replace by query: if a saved search exists for this exact query, update it.
  async upsertByQuery(query, results, opts = {}) {
    const all = await getAll(STORES.discoveries);
    const existing = all.find((d) => d.query === query);
    if (existing) {
      return put(STORES.discoveries, {
        ...existing,
        results,
        lastRunAt: Date.now(),
        ...(opts.autoTrack !== undefined ? { autoTrack: !!opts.autoTrack } : {}),
      });
    }
    return Discoveries.save(query, results, opts);
  },
  list() { return getAll(STORES.discoveries); },
  get(id) { return get(STORES.discoveries, id); },
  remove(id) { return remove(STORES.discoveries, id); },
  async setAutoTrack(id, autoTrack) {
    const d = await get(STORES.discoveries, id);
    if (!d) return null;
    return put(STORES.discoveries, { ...d, autoTrack: !!autoTrack });
  },
  listTracked() { return getByIndex(STORES.discoveries, 'autoTrack', 1).catch(() => []); },
};
