import { prisma } from '../../config/database';
import { logger } from '../../utils/logger';
import { AppError } from '../../middleware/error-handler';
import { env } from '../../config/env';
import type { Prisma } from '@prisma/client';
import type { CreateTagInput, UpdateTagInput, BulkTagInput } from './tags.schemas';

function generateSlug(name: string): string {
  return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}

export const tagsService = {

  // =========================================================================
  // LIST TAGS
  // =========================================================================
  async listTags() {
    const tags = await prisma.crmTag.findMany({
      orderBy: { name: 'asc' },
    });
    return tags;
  },

  // =========================================================================
  // CREATE TAG
  // =========================================================================
  async createTag(input: CreateTagInput, createdByUserId: string) {
    const slug = generateSlug(input.name);

    // Check uniqueness
    const existing = await prisma.crmTag.findFirst({
      where: { OR: [{ name: input.name }, { slug }] },
    });
    if (existing) {
      throw new AppError(409, `Tag "${input.name}" already exists`, 'TAG_EXISTS');
    }

    // Count existing contacts with this tag name
    const countResult = await prisma.$queryRaw<{ count: bigint }[]>`
      SELECT COUNT(*) as count
      FROM contacts
      WHERE "mergedIntoId" IS NULL
        AND tags @> ${JSON.stringify([input.name])}::jsonb
    `;
    const contactCount = Number(countResult[0]?.count ?? 0);

    // Optionally create a Listmonk list for this tag
    let listmonkListId: number | null = null;
    if (input.syncToListmonk && env.LISTMONK_SYNC_ENABLED === 'true') {
      try {
        const { listmonkClient } = await import('../../services/listmonk.client');
        const list = await listmonkClient.createList(
          `CRM: ${input.name}`,
          'private',
          ['v2', 'crm-tag'],
        );
        listmonkListId = list.id;
        logger.info(`Created Listmonk list for CRM tag "${input.name}" (listId=${list.id})`);
      } catch (err) {
        logger.warn(`Failed to create Listmonk list for tag "${input.name}":`, err);
        // Don't fail tag creation if Listmonk is unreachable
      }
    }

    const tag = await prisma.crmTag.create({
      data: {
        name: input.name,
        slug,
        description: input.description || null,
        color: input.color || null,
        listmonkListId,
        contactCount,
        createdByUserId,
      },
    });

    return tag;
  },

  // =========================================================================
  // UPDATE TAG
  // =========================================================================
  async updateTag(id: string, input: UpdateTagInput) {
    const existing = await prisma.crmTag.findUnique({ where: { id } });
    if (!existing) throw new AppError(404, 'Tag not found', 'NOT_FOUND');

    const updateData: Prisma.CrmTagUncheckedUpdateInput = {};

    if (input.name !== undefined && input.name !== existing.name) {
      const newSlug = generateSlug(input.name);

      // Check uniqueness of new name/slug
      const conflict = await prisma.crmTag.findFirst({
        where: {
          OR: [{ name: input.name }, { slug: newSlug }],
          NOT: { id },
        },
      });
      if (conflict) {
        throw new AppError(409, `Tag "${input.name}" already exists`, 'TAG_EXISTS');
      }

      // Rename tag on all contacts (raw SQL for JSONB manipulation)
      await prisma.$executeRaw`
        UPDATE contacts
        SET tags = (
          SELECT jsonb_agg(
            CASE WHEN elem = ${existing.name} THEN ${input.name} ELSE elem END
          )
          FROM jsonb_array_elements_text(tags) AS elem
        ),
        "updatedAt" = NOW()
        WHERE tags @> ${JSON.stringify([existing.name])}::jsonb
          AND "mergedIntoId" IS NULL
      `;

      // Rename Listmonk list if linked
      if (existing.listmonkListId && env.LISTMONK_SYNC_ENABLED === 'true') {
        try {
          const { listmonkClient } = await import('../../services/listmonk.client');
          await listmonkClient.updateList(existing.listmonkListId, {
            name: `CRM: ${input.name}`,
          });
        } catch (err) {
          logger.warn(`Failed to rename Listmonk list for tag "${existing.name}":`, err);
        }
      }

      updateData.name = input.name;
      updateData.slug = newSlug;
    }

    if (input.description !== undefined) updateData.description = input.description;
    if (input.color !== undefined) updateData.color = input.color;

    const tag = await prisma.crmTag.update({
      where: { id },
      data: updateData,
    });

    return tag;
  },

  // =========================================================================
  // DELETE TAG
  // =========================================================================
  async deleteTag(id: string, options: { removeFromContacts?: boolean; deleteListmonkList?: boolean }) {
    const existing = await prisma.crmTag.findUnique({ where: { id } });
    if (!existing) throw new AppError(404, 'Tag not found', 'NOT_FOUND');

    // Remove tag from all contacts
    if (options.removeFromContacts) {
      await prisma.$executeRaw`
        UPDATE contacts
        SET tags = (
          SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
          FROM jsonb_array_elements_text(tags) AS elem
          WHERE elem != ${existing.name}
        ),
        "updatedAt" = NOW()
        WHERE tags @> ${JSON.stringify([existing.name])}::jsonb
          AND "mergedIntoId" IS NULL
      `;
    }

    // Delete Listmonk list
    if (options.deleteListmonkList && existing.listmonkListId && env.LISTMONK_SYNC_ENABLED === 'true') {
      try {
        const { listmonkClient } = await import('../../services/listmonk.client');
        await listmonkClient.deleteList(existing.listmonkListId);
        logger.info(`Deleted Listmonk list ${existing.listmonkListId} for tag "${existing.name}"`);
      } catch (err) {
        logger.warn(`Failed to delete Listmonk list for tag "${existing.name}":`, err);
      }
    }

    await prisma.crmTag.delete({ where: { id } });
  },

  // =========================================================================
  // DISCOVER UNREGISTERED TAGS
  // =========================================================================
  async getUnregisteredTags() {
    // Find all distinct tags in use on contacts that aren't in the registry
    const results = await prisma.$queryRaw<{ name: string; count: bigint }[]>`
      SELECT tag_name AS name, COUNT(*) AS count
      FROM contacts, jsonb_array_elements_text(tags) AS tag_name
      WHERE "mergedIntoId" IS NULL
        AND tag_name NOT IN (SELECT name FROM crm_tags)
      GROUP BY tag_name
      ORDER BY count DESC, tag_name ASC
    `;

    return results.map(r => ({ name: r.name, count: Number(r.count) }));
  },

  // =========================================================================
  // REGISTER ALL UNREGISTERED TAGS
  // =========================================================================
  async registerExistingTags(options: { syncToListmonk?: boolean }, createdByUserId: string) {
    const unregistered = await this.getUnregisteredTags();
    const created: string[] = [];

    for (const { name, count } of unregistered) {
      const slug = generateSlug(name);

      // Skip if slug conflicts
      const conflict = await prisma.crmTag.findFirst({
        where: { OR: [{ name }, { slug }] },
      });
      if (conflict) continue;

      let listmonkListId: number | null = null;
      if (options.syncToListmonk && env.LISTMONK_SYNC_ENABLED === 'true') {
        try {
          const { listmonkClient } = await import('../../services/listmonk.client');
          const list = await listmonkClient.createList(`CRM: ${name}`, 'private', ['v2', 'crm-tag']);
          listmonkListId = list.id;
        } catch (err) {
          logger.warn(`Failed to create Listmonk list for tag "${name}":`, err);
        }
      }

      await prisma.crmTag.create({
        data: {
          name,
          slug,
          listmonkListId,
          contactCount: Number(count),
          createdByUserId,
        },
      });
      created.push(name);
    }

    return { registered: created.length, tags: created };
  },

  // =========================================================================
  // BULK ADD TAG
  // =========================================================================
  async bulkAddTag(input: BulkTagInput) {
    const { tagName, contactIds } = input;

    // Ensure the tag is registered
    let crmTag = await prisma.crmTag.findUnique({ where: { name: tagName } });

    // Add tag to all specified contacts (only if they don't already have it)
    const updated = await prisma.$executeRaw`
      UPDATE contacts
      SET tags = tags || ${JSON.stringify([tagName])}::jsonb,
          "updatedAt" = NOW()
      WHERE id = ANY(${contactIds}::text[])
        AND "mergedIntoId" IS NULL
        AND NOT (tags @> ${JSON.stringify([tagName])}::jsonb)
    `;

    // Recalculate count
    if (crmTag) {
      await this.recalculateCounts(crmTag.id);
    }

    // Listmonk sync for contacts with email
    if (crmTag?.listmonkListId && env.LISTMONK_SYNC_ENABLED === 'true') {
      const contacts = await prisma.contact.findMany({
        where: { id: { in: contactIds }, email: { not: null }, mergedIntoId: null },
        select: { email: true, displayName: true },
      });
      const { listmonkClient } = await import('../../services/listmonk.client');
      for (const c of contacts) {
        try {
          await listmonkClient.upsertSubscriber(
            c.email!,
            c.displayName || '',
            [crmTag.listmonkListId],
            { source: 'crm_tag_bulk', tag_name: tagName, last_synced: new Date().toISOString() },
          );
        } catch (err) {
          logger.warn(`Failed to sync bulk tag for ${c.email}:`, err);
        }
      }
    }

    return { updated: Number(updated) };
  },

  // =========================================================================
  // BULK REMOVE TAG
  // =========================================================================
  async bulkRemoveTag(input: BulkTagInput) {
    const { tagName, contactIds } = input;

    // Remove tag from specified contacts
    const updated = await prisma.$executeRaw`
      UPDATE contacts
      SET tags = (
        SELECT COALESCE(jsonb_agg(elem), '[]'::jsonb)
        FROM jsonb_array_elements_text(tags) AS elem
        WHERE elem != ${tagName}
      ),
      "updatedAt" = NOW()
      WHERE id = ANY(${contactIds}::text[])
        AND "mergedIntoId" IS NULL
        AND tags @> ${JSON.stringify([tagName])}::jsonb
    `;

    // Recalculate count
    const crmTag = await prisma.crmTag.findUnique({ where: { name: tagName } });
    if (crmTag) {
      await this.recalculateCounts(crmTag.id);
    }

    // Listmonk unsync for contacts with email
    if (crmTag?.listmonkListId && env.LISTMONK_SYNC_ENABLED === 'true') {
      const contacts = await prisma.contact.findMany({
        where: { id: { in: contactIds }, email: { not: null }, mergedIntoId: null },
        select: { email: true },
      });
      const { listmonkClient } = await import('../../services/listmonk.client');
      for (const c of contacts) {
        try {
          const subscriber = await listmonkClient.findSubscriberByEmail(c.email!);
          if (subscriber) {
            const currentListIds = subscriber.lists.map(l => l.id);
            await listmonkClient.removeSubscriberFromLists(
              subscriber.id,
              [crmTag.listmonkListId],
              c.email!,
              currentListIds,
            );
          }
        } catch (err) {
          logger.warn(`Failed to unsync bulk tag for ${c.email}:`, err);
        }
      }
    }

    return { updated: Number(updated) };
  },

  // =========================================================================
  // SYNC TAG TO LISTMONK
  // =========================================================================
  async syncTagToListmonk(id: string) {
    const tag = await prisma.crmTag.findUnique({ where: { id } });
    if (!tag) throw new AppError(404, 'Tag not found', 'NOT_FOUND');
    if (!tag.listmonkListId) throw new AppError(400, 'Tag is not linked to a Listmonk list', 'NO_LIST');

    if (env.LISTMONK_SYNC_ENABLED !== 'true') {
      throw new AppError(400, 'Listmonk sync is disabled', 'LISTMONK_DISABLED');
    }

    const { listmonkClient } = await import('../../services/listmonk.client');

    const contacts = await prisma.$queryRaw<{ email: string; displayName: string }[]>`
      SELECT email, "displayName"
      FROM contacts
      WHERE "mergedIntoId" IS NULL
        AND email IS NOT NULL
        AND tags @> ${JSON.stringify([tag.name])}::jsonb
    `;

    let synced = 0;
    let failed = 0;

    for (const c of contacts) {
      try {
        await listmonkClient.upsertSubscriber(
          c.email,
          c.displayName || '',
          [tag.listmonkListId],
          { source: 'crm_tag_sync', tag_name: tag.name, last_synced: new Date().toISOString() },
        );
        synced++;
      } catch (err) {
        failed++;
        logger.warn(`Failed to sync tag "${tag.name}" for ${c.email}:`, err);
      }
    }

    return { total: contacts.length, synced, failed };
  },

  // =========================================================================
  // SYNC ALL TAGS TO LISTMONK
  // =========================================================================
  async syncAllTagsToListmonk() {
    if (env.LISTMONK_SYNC_ENABLED !== 'true') {
      throw new AppError(400, 'Listmonk sync is disabled', 'LISTMONK_DISABLED');
    }

    const tags = await prisma.crmTag.findMany({
      where: { listmonkListId: { not: null } },
    });

    const results: { tagName: string; total: number; synced: number; failed: number }[] = [];

    for (const tag of tags) {
      try {
        const result = await this.syncTagToListmonk(tag.id);
        results.push({ tagName: tag.name, ...result });
      } catch (err) {
        logger.warn(`Failed to sync all for tag "${tag.name}":`, err);
        results.push({ tagName: tag.name, total: 0, synced: 0, failed: 0 });
      }
    }

    // Also recalculate counts
    await this.recalculateCounts();

    return { tags: results };
  },

  // =========================================================================
  // RECALCULATE COUNTS
  // =========================================================================
  async recalculateCounts(tagId?: string) {
    const where = tagId ? { id: tagId } : {};
    const tags = await prisma.crmTag.findMany({ where });

    for (const tag of tags) {
      const countResult = await prisma.$queryRaw<{ count: bigint }[]>`
        SELECT COUNT(*) as count
        FROM contacts
        WHERE "mergedIntoId" IS NULL
          AND tags @> ${JSON.stringify([tag.name])}::jsonb
      `;
      await prisma.crmTag.update({
        where: { id: tag.id },
        data: { contactCount: Number(countResult[0]?.count ?? 0) },
      });
    }
  },

  // =========================================================================
  // UPDATE TAG COUNTS (increment/decrement helpers for hooks)
  // =========================================================================
  async updateTagCounts(addedTags: string[], removedTags: string[]) {
    if (addedTags.length > 0) {
      await prisma.crmTag.updateMany({
        where: { name: { in: addedTags } },
        data: { contactCount: { increment: 1 } },
      });
    }
    if (removedTags.length > 0) {
      await prisma.crmTag.updateMany({
        where: { name: { in: removedTags } },
        data: { contactCount: { decrement: 1 } },
      });
    }
  },
};
