"use server";

import { revalidatePath } from "next/cache";
import { AppwriteException, ID, Permission, Query, Role } from "node-appwrite";
import { z } from "zod";

import { logAudit } from "./audit";
import { syncFinanceForJob } from "./finance-sync";
import { archiveJobFiles } from "./job-file-archive";
import { createNotification } from "./notification-helpers";
import { calculateJobPriceForProsthetic } from "./pricing";
import {
  DATABASE_ID,
  TABLES,
  type Connection,
  type Job,
  type JobStep,
  type Patient,
  type Prosthetic,
} from "./schema";
import { createAdminClient } from "./server";
import { requireRole, requireTenant, requireTenantKind } from "./tenant-guard";
import { JOB_STEP_ORDER } from "./job-types";
import type { JobActionState, JobFormState } from "./job-types";
import { createJobSchema } from "@/lib/validation/job";

function appwriteError(e: unknown, fallback = "Beklenmeyen bir hata oluştu."): string {
  if (e instanceof AppwriteException) return e.message || fallback;
  return process.env.NODE_ENV !== "production" && e instanceof Error
    ? `${fallback} (${e.message})`
    : fallback;
}

function flattenErrors(err: z.ZodError): Record<string, string> {
  const out: Record<string, string> = {};
  for (const issue of err.issues) {
    const key = issue.path.join(".");
    if (key && !out[key]) out[key] = issue.message;
  }
  return out;
}

function pickFields(formData: FormData) {
  return {
    labTenantId: String(formData.get("labTenantId") ?? "").trim(),
    patientId: String(formData.get("patientId") ?? "").trim(),
    patientCode: String(formData.get("patientCode") ?? "").trim(),
    prostheticId: String(formData.get("prostheticId") ?? "").trim(),
    teeth: formData.getAll("teeth").map((v) => String(v).trim()).filter(Boolean),
    color: String(formData.get("color") ?? "").trim(),
    description: String(formData.get("description") ?? "").trim(),
    dueDate: String(formData.get("dueDate") ?? "").trim(),
  };
}

function jobPermissions(clinicTenantId: string, labTenantId: string): string[] {
  return [
    Permission.read(Role.team(clinicTenantId)),
    Permission.read(Role.team(labTenantId)),
    Permission.update(Role.team(clinicTenantId, "owner")),
    Permission.update(Role.team(clinicTenantId, "admin")),
    Permission.update(Role.team(clinicTenantId, "member")),
    Permission.update(Role.team(labTenantId, "owner")),
    Permission.update(Role.team(labTenantId, "admin")),
    Permission.update(Role.team(labTenantId, "member")),
    Permission.delete(Role.team(clinicTenantId, "owner")),
    Permission.delete(Role.team(clinicTenantId, "admin")),
  ];
}

export async function createJobAction(
  _prev: JobFormState,
  formData: FormData,
): Promise<JobFormState> {
  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin", "member"]);
    requireTenantKind(ctx, ["clinic"]);
  } catch {
    return { ok: false, error: "İş yayınlama yalnızca klinik hesapları için." };
  }

  const parsed = createJobSchema.safeParse(pickFields(formData));
  if (!parsed.success) {
    return {
      ok: false,
      error: "Form geçersiz.",
      fieldErrors: flattenErrors(parsed.error),
    };
  }

  const { tablesDB } = createAdminClient();

  // If a patientId is supplied, verify it belongs to this clinic and inherit
  // its patientCode so the lab side always sees a stable identifier.
  let patientCode = parsed.data.patientCode;
  if (parsed.data.patientId) {
    try {
      const patientRow = (await tablesDB.getRow(
        DATABASE_ID,
        TABLES.patients,
        parsed.data.patientId,
      )) as unknown as Patient;
      if (patientRow.clinicTenantId !== ctx.tenantId) {
        return {
          ok: false,
          error: "Bu hasta size ait değil.",
          fieldErrors: { patientId: "Yetki yok." },
        };
      }
      patientCode = patientRow.patientCode;
    } catch {
      return {
        ok: false,
        error: "Hasta bulunamadı.",
        fieldErrors: { patientId: "Hasta kaydı yok." },
      };
    }
  }

  // Verify the chosen lab is an approved connection of this clinic
  const connRes = await tablesDB.listRows({
    databaseId: DATABASE_ID,
    tableId: TABLES.connections,
    queries: [
      Query.equal("clinicTenantId", ctx.tenantId),
      Query.equal("labTenantId", parsed.data.labTenantId),
      Query.equal("status", "approved"),
      Query.limit(1),
    ],
  });
  const conn = connRes.rows[0] as unknown as Connection | undefined;
  if (!conn) {
    return {
      ok: false,
      error: "Seçilen laboratuvarla onaylanmış bir bağlantınız yok.",
      fieldErrors: { labTenantId: "Onaylı bağlantı bulunamadı." },
    };
  }

  // Resolve the chosen catalog product. It must belong to the selected lab
  // and still be active — anything else is rejected with a clear error.
  let prosthetic: Prosthetic;
  try {
    const row = await tablesDB.getRow(
      DATABASE_ID,
      TABLES.prosthetics,
      parsed.data.prostheticId,
    );
    prosthetic = row as unknown as Prosthetic;
  } catch {
    return {
      ok: false,
      error: "Seçilen ürün bulunamadı.",
      fieldErrors: { prostheticId: "Ürün bulunamadı." },
    };
  }
  if (prosthetic.tenantId !== parsed.data.labTenantId) {
    return {
      ok: false,
      error: "Seçilen ürün bu laboratuvara ait değil.",
      fieldErrors: { prostheticId: "Ürün bu lab'a ait değil." },
    };
  }
  if (prosthetic.archived) {
    return {
      ok: false,
      error: "Seçilen ürün arşivlenmiş.",
      fieldErrors: { prostheticId: "Bu ürün artık aktif değil." },
    };
  }

  try {
    // Server-side price calculation — clinic never sets the price.
    const quote = await calculateJobPriceForProsthetic({
      prosthetic,
      clinicTenantId: ctx.tenantId,
      teethCount: parsed.data.teeth.length,
    });

    const created = await tablesDB.createRow(
      DATABASE_ID,
      TABLES.jobs,
      ID.unique(),
      {
        clinicTenantId: ctx.tenantId,
        labTenantId: parsed.data.labTenantId,
        createdBy: ctx.user.id,
        patientId: parsed.data.patientId,
        patientCode,
        prostheticType: prosthetic.type,
        prostheticId: prosthetic.$id,
        memberCount: parsed.data.teeth.length,
        teeth: parsed.data.teeth,
        color: parsed.data.color,
        description: parsed.data.description,
        price: quote?.amount,
        currency: quote?.currency,
        dueDate: parsed.data.dueDate,
        status: "pending",
      },
      jobPermissions(ctx.tenantId, parsed.data.labTenantId),
    );
    void logAudit({
      tenantId: ctx.tenantId,
      userId: ctx.user.id,
      action: "create",
      entityType: "job",
      entityId: created.$id,
      changes: { labTenantId: parsed.data.labTenantId, patientCode },
    });
    void createNotification({
      tenantId: parsed.data.labTenantId,
      jobId: created.$id,
      message: `${ctx.settings?.companyName ?? "Bir klinik"} yeni iş yayınladı (${patientCode}).`,
    });
    revalidatePath("/jobs/outbound");
    revalidatePath("/dashboard");
    return { ok: true, jobId: created.$id };
  } catch (e) {
    return { ok: false, error: appwriteError(e, "İş oluşturulamadı.") };
  }
}

function jobHistoryPermissions(clinicTenantId: string, labTenantId: string): string[] {
  return [
    Permission.read(Role.team(clinicTenantId)),
    Permission.read(Role.team(labTenantId)),
  ];
}

async function loadJobForTenant(jobId: string, tenantId: string): Promise<Job | null> {
  try {
    const { tablesDB } = createAdminClient();
    const row = await tablesDB.getRow(DATABASE_ID, TABLES.jobs, jobId);
    const job = row as unknown as Job;
    if (job.clinicTenantId !== tenantId && job.labTenantId !== tenantId) {
      return null;
    }
    return job;
  } catch {
    return null;
  }
}

async function appendJobHistory(args: {
  job: Job;
  step: JobStep;
  completedBy: string;
  note?: string;
}): Promise<void> {
  const { tablesDB } = createAdminClient();
  try {
    await tablesDB.createRow(
      DATABASE_ID,
      TABLES.jobStatusHistory,
      ID.unique(),
      {
        jobId: args.job.$id,
        clinicTenantId: args.job.clinicTenantId,
        labTenantId: args.job.labTenantId,
        step: args.step,
        completedBy: args.completedBy,
        completedAt: new Date().toISOString(),
        note: args.note,
      },
      jobHistoryPermissions(args.job.clinicTenantId, args.job.labTenantId),
    );
  } catch {
    // history failures must never block the main mutation
  }
}

export async function acceptJobAction(
  _prev: JobActionState,
  formData: FormData,
): Promise<JobActionState> {
  const jobId = String(formData.get("jobId") ?? "").trim();
  if (!jobId) return { ok: false, error: "İş bulunamadı." };

  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin", "member"]);
    requireTenantKind(ctx, ["lab"]);
  } catch {
    return { ok: false, error: "Sadece laboratuvar bu işi kabul edebilir." };
  }

  const job = await loadJobForTenant(jobId, ctx.tenantId);
  if (!job || job.labTenantId !== ctx.tenantId) {
    return { ok: false, error: "İş bulunamadı." };
  }
  if (job.status !== "pending") {
    return { ok: false, error: "Bu iş zaten işleme alınmış." };
  }

  try {
    const { tablesDB } = createAdminClient();
    // Accepting the job = lab took the impression, started substructure work.
    // Step jumps straight to alt_yapi_prova; location flips to at_lab.
    await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
      status: "in_progress",
      currentStep: "alt_yapi_prova",
      location: "at_lab",
    });
    await appendJobHistory({ job, step: "olcu", completedBy: ctx.user.id });
    void logAudit({
      tenantId: ctx.tenantId,
      userId: ctx.user.id,
      action: "update",
      entityType: "job",
      entityId: jobId,
      changes: {
        status: "in_progress",
        currentStep: "alt_yapi_prova",
        location: "at_lab",
      },
    });
    void createNotification({
      tenantId: job.clinicTenantId,
      jobId,
      message: `${ctx.settings?.companyName ?? "Lab"} hasta ${job.patientCode} işini işleme aldı, alt yapı üretiminde.`,
    });
  } catch (e) {
    return { ok: false, error: appwriteError(e, "Kabul edilemedi.") };
  }

  revalidatePath(`/jobs/${jobId}`);
  revalidatePath("/jobs/inbound");
  revalidatePath("/jobs/outbound");
  return { ok: true };
}

/**
 * Lab hands the work back to the clinic for the next physical step
 * (prova or final delivery). The current step stays the same — only the
 * location flips at_lab → at_clinic. If the lab is finishing the last
 * production step (cila_bitim), that's the final delivery and the job
 * status becomes "sent".
 */
export async function handToClinicAction(
  _prev: JobActionState,
  formData: FormData,
): Promise<JobActionState> {
  const jobId = String(formData.get("jobId") ?? "").trim();
  if (!jobId) return { ok: false, error: "İş bulunamadı." };
  const note = String(formData.get("note") ?? "").trim() || undefined;

  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin", "member"]);
    requireTenantKind(ctx, ["lab"]);
  } catch {
    return { ok: false, error: "Sadece laboratuvar kliniğe gönderebilir." };
  }

  const job = await loadJobForTenant(jobId, ctx.tenantId);
  if (!job || job.labTenantId !== ctx.tenantId) {
    return { ok: false, error: "İş bulunamadı." };
  }
  if (job.status !== "in_progress") {
    return { ok: false, error: "Sadece işlemdeki işler kliniğe gönderilebilir." };
  }
  if (job.location !== "at_lab") {
    return { ok: false, error: "İş zaten kliniğe gönderilmiş." };
  }
  if (!job.currentStep) {
    return { ok: false, error: "Mevcut aşama bilinmiyor." };
  }

  const isFinalStep = job.currentStep === "cila_bitim";

  try {
    const { tablesDB } = createAdminClient();
    if (isFinalStep) {
      // Final delivery — production is done, status moves to sent.
      await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
        status: "sent",
        location: "at_clinic",
      });
      await appendJobHistory({
        job,
        step: "cila_bitim",
        completedBy: ctx.user.id,
        note,
      });
      void logAudit({
        tenantId: ctx.tenantId,
        userId: ctx.user.id,
        action: "update",
        entityType: "job",
        entityId: jobId,
        changes: { status: "sent", location: "at_clinic" },
      });
      void syncFinanceForJob({ ...job, status: "sent" });
      void createNotification({
        tenantId: job.clinicTenantId,
        jobId,
        message: `Hasta ${job.patientCode} cila/bitim tamamlandı, nihai teslime gönderildi.`,
      });
    } else {
      // Prova için klinike geçici teslim — step aynı, location değişti.
      await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
        location: "at_clinic",
      });
      await appendJobHistory({
        job,
        step: job.currentStep,
        completedBy: ctx.user.id,
        note,
      });
      void logAudit({
        tenantId: ctx.tenantId,
        userId: ctx.user.id,
        action: "update",
        entityType: "job",
        entityId: jobId,
        changes: { location: "at_clinic", handedOffStep: job.currentStep },
      });
      const stepLabel =
        job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı";
      void createNotification({
        tenantId: job.clinicTenantId,
        jobId,
        message: `Hasta ${job.patientCode} ${stepLabel} provasına hazır, kliniğe gönderildi.`,
      });
    }
  } catch (e) {
    return { ok: false, error: appwriteError(e, "Gönderilemedi.") };
  }

  revalidatePath(`/jobs/${jobId}`);
  revalidatePath("/jobs/inbound");
  revalidatePath("/jobs/outbound");
  revalidatePath("/finance");
  return { ok: true };
}

/**
 * Clinic confirms the prova was successful. Step advances to the next
 * production stage and location flips back at_clinic → at_lab so the
 * lab can pick the work back up.
 */
export async function approveAtClinicAction(
  _prev: JobActionState,
  formData: FormData,
): Promise<JobActionState> {
  const jobId = String(formData.get("jobId") ?? "").trim();
  if (!jobId) return { ok: false, error: "İş bulunamadı." };
  const note = String(formData.get("note") ?? "").trim() || undefined;

  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin", "member"]);
    requireTenantKind(ctx, ["clinic"]);
  } catch {
    return { ok: false, error: "Sadece klinik provayı onaylayabilir." };
  }

  const job = await loadJobForTenant(jobId, ctx.tenantId);
  if (!job || job.clinicTenantId !== ctx.tenantId) {
    return { ok: false, error: "İş bulunamadı." };
  }
  if (job.status !== "in_progress") {
    return { ok: false, error: "Yalnızca işlemdeki provalar onaylanabilir." };
  }
  if (job.location !== "at_clinic") {
    return { ok: false, error: "İş şu an klinikte değil." };
  }
  if (!job.currentStep) {
    return { ok: false, error: "Mevcut aşama bilinmiyor." };
  }

  const currentIdx = JOB_STEP_ORDER.indexOf(job.currentStep);
  const nextStep = JOB_STEP_ORDER[currentIdx + 1];
  if (!nextStep) {
    return { ok: false, error: "Bu aşamadan ileri gidilemez." };
  }

  try {
    const { tablesDB } = createAdminClient();
    await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
      currentStep: nextStep,
      location: "at_lab",
    });
    await appendJobHistory({
      job,
      step: job.currentStep,
      completedBy: ctx.user.id,
      note,
    });
    void logAudit({
      tenantId: ctx.tenantId,
      userId: ctx.user.id,
      action: "update",
      entityType: "job",
      entityId: jobId,
      changes: {
        currentStep: nextStep,
        location: "at_lab",
        completedStep: job.currentStep,
      },
    });
    const stepLabel =
      job.currentStep === "alt_yapi_prova" ? "alt yapı" : "üst yapı";
    void createNotification({
      tenantId: job.labTenantId,
      jobId,
      message: `Hasta ${job.patientCode} ${stepLabel} provası onaylandı, lab tarafına geri döndü.`,
    });
  } catch (e) {
    return { ok: false, error: appwriteError(e, "Onaylanamadı.") };
  }

  revalidatePath(`/jobs/${jobId}`);
  revalidatePath("/jobs/inbound");
  revalidatePath("/jobs/outbound");
  return { ok: true };
}

/**
 * Clinic rejects the prova and asks the lab to redo this stage. The job
 * goes back to the lab without advancing the step, so the same prova
 * stage will repeat after the lab finishes the rework. A note explaining
 * what's wrong is required — there's no point bouncing a case back
 * without telling the lab what to fix.
 */
export async function requestRevisionAction(
  _prev: JobActionState,
  formData: FormData,
): Promise<JobActionState> {
  const jobId = String(formData.get("jobId") ?? "").trim();
  if (!jobId) return { ok: false, error: "İş bulunamadı." };
  const note = String(formData.get("note") ?? "").trim();
  if (!note) {
    return {
      ok: false,
      error: "Düzeltme talebi için neyin yanlış olduğunu yazmanız gerek.",
    };
  }

  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin", "member"]);
    requireTenantKind(ctx, ["clinic"]);
  } catch {
    return { ok: false, error: "Düzeltme talebini yalnızca klinik açabilir." };
  }

  const job = await loadJobForTenant(jobId, ctx.tenantId);
  if (!job || job.clinicTenantId !== ctx.tenantId) {
    return { ok: false, error: "İş bulunamadı." };
  }
  if (job.status !== "in_progress") {
    return { ok: false, error: "Yalnızca işlemdeki provalar için düzeltme istenebilir." };
  }
  if (job.location !== "at_clinic") {
    return { ok: false, error: "İş şu an klinikte değil." };
  }
  if (!job.currentStep) {
    return { ok: false, error: "Mevcut aşama bilinmiyor." };
  }

  try {
    const { tablesDB } = createAdminClient();
    await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
      location: "at_lab",
      // currentStep stays the same — the lab will rework this stage.
    });
    await appendJobHistory({
      job,
      step: job.currentStep,
      completedBy: ctx.user.id,
      note: `[Düzeltme talebi] ${note}`,
    });
    void logAudit({
      tenantId: ctx.tenantId,
      userId: ctx.user.id,
      action: "update",
      entityType: "job",
      entityId: jobId,
      changes: {
        location: "at_lab",
        revisionRequestedAtStep: job.currentStep,
        note,
      },
    });
    const stepLabel =
      job.currentStep === "alt_yapi_prova"
        ? "alt yapı"
        : job.currentStep === "ust_yapi_prova"
          ? "üst yapı"
          : "cila/bitim";
    void createNotification({
      tenantId: job.labTenantId,
      jobId,
      message: `Hasta ${job.patientCode} ${stepLabel} provası için düzeltme istendi: ${note.slice(0, 120)}`,
    });
  } catch (e) {
    return { ok: false, error: appwriteError(e, "Düzeltme talebi gönderilemedi.") };
  }

  revalidatePath(`/jobs/${jobId}`);
  revalidatePath("/jobs/inbound");
  revalidatePath("/jobs/outbound");
  return { ok: true };
}

export async function markDeliveredAction(
  _prev: JobActionState,
  formData: FormData,
): Promise<JobActionState> {
  const jobId = String(formData.get("jobId") ?? "").trim();
  if (!jobId) return { ok: false, error: "İş bulunamadı." };

  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin", "member"]);
    requireTenantKind(ctx, ["clinic"]);
  } catch {
    return { ok: false, error: "Teslim almayı yalnızca klinik yapabilir." };
  }

  const job = await loadJobForTenant(jobId, ctx.tenantId);
  if (!job || job.clinicTenantId !== ctx.tenantId) {
    return { ok: false, error: "İş bulunamadı." };
  }
  if (job.status !== "sent") {
    return { ok: false, error: "Sadece gönderilmiş işler teslim alınabilir." };
  }

  try {
    const { tablesDB } = createAdminClient();
    await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
      status: "delivered",
    });
    void logAudit({
      tenantId: ctx.tenantId,
      userId: ctx.user.id,
      action: "update",
      entityType: "job",
      entityId: jobId,
      changes: { status: "delivered" },
    });
    void syncFinanceForJob({ ...job, status: "delivered" });
    void createNotification({
      tenantId: job.labTenantId,
      jobId,
      message: `Hasta ${job.patientCode} işi teslim alındı.`,
    });
    // Free up Storage now that the case is closed. Metadata rows stay for
    // the audit trail; only the binaries go.
    void archiveJobFiles(jobId);
  } catch (e) {
    return { ok: false, error: appwriteError(e, "Teslim alınamadı.") };
  }

  revalidatePath(`/jobs/${jobId}`);
  revalidatePath("/jobs/outbound");
  revalidatePath("/jobs/inbound");
  revalidatePath("/finance");
  return { ok: true };
}

export async function cancelJobAction(
  _prev: JobActionState,
  formData: FormData,
): Promise<JobActionState> {
  const jobId = String(formData.get("jobId") ?? "").trim();
  if (!jobId) return { ok: false, error: "İş bulunamadı." };

  let ctx;
  try {
    ctx = await requireTenant();
    requireRole(ctx, ["owner", "admin"]);
  } catch {
    return { ok: false, error: "Bu işlem için yetkiniz yok." };
  }

  const job = await loadJobForTenant(jobId, ctx.tenantId);
  if (!job) return { ok: false, error: "İş bulunamadı." };
  if (job.status !== "pending") {
    return { ok: false, error: "Yalnızca bekleyen işler iptal edilebilir." };
  }
  if (ctx.kind === "clinic" && job.clinicTenantId !== ctx.tenantId) {
    return { ok: false, error: "Yetkiniz yok." };
  }
  if (ctx.kind === "lab" && job.labTenantId !== ctx.tenantId) {
    return { ok: false, error: "Yetkiniz yok." };
  }

  try {
    const { tablesDB } = createAdminClient();
    await tablesDB.updateRow(DATABASE_ID, TABLES.jobs, jobId, {
      status: "cancelled",
    });
    void logAudit({
      tenantId: ctx.tenantId,
      userId: ctx.user.id,
      action: "update",
      entityType: "job",
      entityId: jobId,
      changes: { status: "cancelled" },
    });
  } catch (e) {
    return { ok: false, error: appwriteError(e, "İptal edilemedi.") };
  }

  revalidatePath(`/jobs/${jobId}`);
  revalidatePath("/jobs/inbound");
  revalidatePath("/jobs/outbound");
  return { ok: true };
}
