import { Injectable } from "@nestjs/common";
import { Prisma, PrismaClient, ProjectStatus, TaskPriority, TaskStatus } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import type { ImportProjectDto, ImportResponseDto, ImportTaskDto } from "./dto";

interface TaskStatusMapping {
  status: TaskStatus;
  issue: string | null;
}

interface TaskPriorityMapping {
  priority: TaskPriority;
  issue: string | null;
}

interface ProjectStatusMapping {
  status: ProjectStatus;
  issue: string | null;
}

@Injectable()
export class ImportService {
  constructor(private readonly prisma: PrismaService) {}

  async importTasks(
    workspaceId: string,
    userId: string,
    taskPayload: ImportTaskDto[]
  ): Promise<ImportResponseDto> {
    const errors: string[] = [];
    let imported = 0;
    let skipped = 0;

    const importTimestamp = new Date().toISOString();
    const seenBrainTaskIds = new Set<string>();
    const domainIdBySlug = new Map<string, string>();
    const projectIdByBrainId = new Map<string, string | null>();

    await this.prisma.withWorkspaceContext(userId, workspaceId, async (tx: PrismaClient) => {
      for (const [index, task] of taskPayload.entries()) {
        const brainId = task.id.trim();

        if (seenBrainTaskIds.has(brainId)) {
          skipped += 1;
          errors.push(`task ${brainId}: duplicate item in request body`);
          continue;
        }
        seenBrainTaskIds.add(brainId);

        try {
          const existingTask = await tx.task.findFirst({
            where: {
              workspaceId,
              metadata: {
                path: ["brainId"],
                equals: brainId,
              },
            },
            select: { id: true },
          });

          if (existingTask) {
            skipped += 1;
            continue;
          }

          const mappedStatus = this.mapTaskStatus(task.status ?? null);
          if (mappedStatus.issue) {
            errors.push(`task ${brainId}: ${mappedStatus.issue}`);
          }

          const mappedPriority = this.mapTaskPriority(task.priority ?? null);
          if (mappedPriority.issue) {
            errors.push(`task ${brainId}: ${mappedPriority.issue}`);
          }

          const projectBrainId = task.project?.trim() ? task.project.trim() : null;
          const projectId = await this.resolveProjectId(
            tx,
            workspaceId,
            projectBrainId,
            projectIdByBrainId,
            brainId,
            errors
          );

          const domainId = await this.resolveDomainId(
            tx,
            workspaceId,
            task.domain ?? null,
            importTimestamp,
            domainIdBySlug
          );

          const createdAt =
            this.normalizeDate(task.created ?? null, `task ${brainId}.created`, errors) ??
            new Date();
          const updatedAt =
            this.normalizeDate(task.updated ?? null, `task ${brainId}.updated`, errors) ??
            createdAt;
          const dueDate = this.normalizeDate(task.due ?? null, `task ${brainId}.due`, errors);
          const completedAt = mappedStatus.status === TaskStatus.COMPLETED ? updatedAt : null;

          const metadata = this.asJsonValue({
            source: "jarvis-brain",
            brainId,
            brainDomain: task.domain ?? null,
            brainProjectId: projectBrainId,
            rawStatus: task.status ?? null,
            rawPriority: task.priority ?? null,
            related: task.related ?? [],
            blocks: task.blocks ?? [],
            blockedBy: task.blocked_by ?? [],
            assignee: task.assignee ?? null,
            progress: task.progress ?? null,
            notes: task.notes ?? null,
            notesNonTechnical: task.notes_nontechnical ?? null,
            importedAt: importTimestamp,
          });

          await tx.task.create({
            data: {
              workspaceId,
              title: task.title,
              description: task.notes ?? null,
              status: mappedStatus.status,
              priority: mappedPriority.priority,
              dueDate,
              creatorId: userId,
              projectId,
              domainId,
              metadata,
              createdAt,
              updatedAt,
              completedAt,
            },
          });

          imported += 1;
        } catch (error) {
          skipped += 1;
          errors.push(
            `task ${brainId || `index-${String(index)}`}: failed to import: ${this.getErrorMessage(error)}`
          );
        }
      }
    });

    return {
      imported,
      skipped,
      errors,
    };
  }

  async importProjects(
    workspaceId: string,
    userId: string,
    projectPayload: ImportProjectDto[]
  ): Promise<ImportResponseDto> {
    const errors: string[] = [];
    let imported = 0;
    let skipped = 0;

    const importTimestamp = new Date().toISOString();
    const seenBrainProjectIds = new Set<string>();
    const domainIdBySlug = new Map<string, string>();

    await this.prisma.withWorkspaceContext(userId, workspaceId, async (tx: PrismaClient) => {
      for (const [index, project] of projectPayload.entries()) {
        const brainId = project.id.trim();

        if (seenBrainProjectIds.has(brainId)) {
          skipped += 1;
          errors.push(`project ${brainId}: duplicate item in request body`);
          continue;
        }
        seenBrainProjectIds.add(brainId);

        try {
          const existingProject = await tx.project.findFirst({
            where: {
              workspaceId,
              metadata: {
                path: ["brainId"],
                equals: brainId,
              },
            },
            select: { id: true },
          });

          if (existingProject) {
            skipped += 1;
            continue;
          }

          const mappedStatus = this.mapProjectStatus(project.status ?? null);
          if (mappedStatus.issue) {
            errors.push(`project ${brainId}: ${mappedStatus.issue}`);
          }

          const domainId = await this.resolveDomainId(
            tx,
            workspaceId,
            project.domain ?? null,
            importTimestamp,
            domainIdBySlug
          );

          const createdAt =
            this.normalizeDate(project.created ?? null, `project ${brainId}.created`, errors) ??
            new Date();
          const updatedAt =
            this.normalizeDate(project.updated ?? null, `project ${brainId}.updated`, errors) ??
            createdAt;
          const startDate = this.normalizeDate(
            project.created ?? null,
            `project ${brainId}.startDate`,
            errors
          );
          const endDate = this.normalizeDate(
            project.target_date ?? null,
            `project ${brainId}.target_date`,
            errors
          );

          const metadata = this.asJsonValue({
            source: "jarvis-brain",
            brainId,
            brainDomain: project.domain ?? null,
            rawStatus: project.status ?? null,
            rawPriority: project.priority ?? null,
            progress: project.progress ?? null,
            repo: project.repo ?? null,
            branch: project.branch ?? null,
            currentMilestone: project.current_milestone ?? null,
            nextMilestone: project.next_milestone ?? null,
            blocker: project.blocker ?? null,
            owner: project.owner ?? null,
            docsPath: project.docs_path ?? null,
            targetDate: project.target_date ?? null,
            notes: project.notes ?? null,
            notesNonTechnical: project.notes_nontechnical ?? null,
            parent: project.parent ?? null,
            importedAt: importTimestamp,
          });

          await tx.project.create({
            data: {
              workspaceId,
              name: project.name,
              description: project.description ?? null,
              status: mappedStatus.status,
              startDate,
              endDate,
              creatorId: userId,
              domainId,
              metadata,
              createdAt,
              updatedAt,
            },
          });

          imported += 1;
        } catch (error) {
          skipped += 1;
          errors.push(
            `project ${brainId || `index-${String(index)}`}: failed to import: ${this.getErrorMessage(error)}`
          );
        }
      }
    });

    return {
      imported,
      skipped,
      errors,
    };
  }

  private async resolveProjectId(
    tx: PrismaClient,
    workspaceId: string,
    projectBrainId: string | null,
    projectIdByBrainId: Map<string, string | null>,
    taskBrainId: string,
    errors: string[]
  ): Promise<string | null> {
    if (!projectBrainId) {
      return null;
    }

    if (projectIdByBrainId.has(projectBrainId)) {
      return projectIdByBrainId.get(projectBrainId) ?? null;
    }

    const existingProject = await tx.project.findFirst({
      where: {
        workspaceId,
        metadata: {
          path: ["brainId"],
          equals: projectBrainId,
        },
      },
      select: { id: true },
    });

    if (!existingProject) {
      projectIdByBrainId.set(projectBrainId, null);
      errors.push(`task ${taskBrainId}: referenced project "${projectBrainId}" not found`);
      return null;
    }

    projectIdByBrainId.set(projectBrainId, existingProject.id);
    return existingProject.id;
  }

  private async resolveDomainId(
    tx: PrismaClient,
    workspaceId: string,
    rawDomain: string | null,
    importTimestamp: string,
    domainIdBySlug: Map<string, string>
  ): Promise<string | null> {
    const domainSlug = this.normalizeDomain(rawDomain);
    if (!domainSlug) {
      return null;
    }

    const cachedId = domainIdBySlug.get(domainSlug);
    if (cachedId) {
      return cachedId;
    }

    const existingDomain = await tx.domain.findUnique({
      where: {
        workspaceId_slug: {
          workspaceId,
          slug: domainSlug,
        },
      },
      select: { id: true },
    });

    if (existingDomain) {
      domainIdBySlug.set(domainSlug, existingDomain.id);
      return existingDomain.id;
    }

    const trimmedDomainName = rawDomain?.trim();
    const domainName =
      trimmedDomainName && trimmedDomainName.length > 0 ? trimmedDomainName : domainSlug;
    const createdDomain = await tx.domain.create({
      data: {
        workspaceId,
        slug: domainSlug,
        name: domainName,
        metadata: this.asJsonValue({
          source: "jarvis-brain",
          brainId: domainName,
          sourceValues: [domainName],
          importedAt: importTimestamp,
        }),
      },
      select: { id: true },
    });

    domainIdBySlug.set(domainSlug, createdDomain.id);
    return createdDomain.id;
  }

  private normalizeKey(value: string | null | undefined): string {
    return value?.trim().toLowerCase() ?? "";
  }

  private mapTaskStatus(rawStatus: string | null): TaskStatusMapping {
    const statusKey = this.normalizeKey(rawStatus);

    switch (statusKey) {
      case "done":
        return { status: TaskStatus.COMPLETED, issue: null };
      case "in-progress":
        return { status: TaskStatus.IN_PROGRESS, issue: null };
      case "backlog":
      case "pending":
      case "scheduled":
      case "not-started":
      case "planned":
        return { status: TaskStatus.NOT_STARTED, issue: null };
      case "blocked":
      case "on-hold":
        return { status: TaskStatus.PAUSED, issue: null };
      case "cancelled":
        return { status: TaskStatus.ARCHIVED, issue: null };
      default:
        return {
          status: TaskStatus.NOT_STARTED,
          issue: `Unknown task status "${rawStatus ?? "null"}" mapped to NOT_STARTED`,
        };
    }
  }

  private mapTaskPriority(rawPriority: string | null): TaskPriorityMapping {
    const priorityKey = this.normalizeKey(rawPriority);

    switch (priorityKey) {
      case "critical":
      case "high":
        return { priority: TaskPriority.HIGH, issue: null };
      case "medium":
        return { priority: TaskPriority.MEDIUM, issue: null };
      case "low":
        return { priority: TaskPriority.LOW, issue: null };
      default:
        return {
          priority: TaskPriority.MEDIUM,
          issue: `Unknown task priority "${rawPriority ?? "null"}" mapped to MEDIUM`,
        };
    }
  }

  private mapProjectStatus(rawStatus: string | null): ProjectStatusMapping {
    const statusKey = this.normalizeKey(rawStatus);

    switch (statusKey) {
      case "active":
      case "in-progress":
        return { status: ProjectStatus.ACTIVE, issue: null };
      case "backlog":
      case "planning":
        return { status: ProjectStatus.PLANNING, issue: null };
      case "paused":
      case "blocked":
        return { status: ProjectStatus.PAUSED, issue: null };
      case "archived":
      case "maintenance":
        return { status: ProjectStatus.ARCHIVED, issue: null };
      default:
        return {
          status: ProjectStatus.PLANNING,
          issue: `Unknown project status "${rawStatus ?? "null"}" mapped to PLANNING`,
        };
    }
  }

  private normalizeDomain(rawDomain: string | null | undefined): string | null {
    if (!rawDomain) {
      return null;
    }

    const trimmed = rawDomain.trim();
    if (trimmed.length === 0) {
      return null;
    }

    const slug = trimmed
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, "-")
      .replace(/^-+|-+$/g, "");

    return slug.length > 0 ? slug : null;
  }

  private normalizeDate(rawValue: string | null, context: string, errors: string[]): Date | null {
    if (!rawValue) {
      return null;
    }

    const trimmed = rawValue.trim();
    if (trimmed.length === 0) {
      return null;
    }

    const value = /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}T00:00:00.000Z` : trimmed;
    const parsedDate = new Date(value);

    if (Number.isNaN(parsedDate.getTime())) {
      errors.push(`${context}: invalid date "${rawValue}"`);
      return null;
    }

    return parsedDate;
  }

  private asJsonValue(value: Record<string, unknown>): Prisma.InputJsonValue {
    return value as Prisma.InputJsonValue;
  }

  private getErrorMessage(error: unknown): string {
    if (error instanceof Error) {
      return error.message;
    }

    return String(error);
  }
}
