import { timingSafeEqual } from "node:crypto";
import {
  checkCode,
  generateCode,
  generateToken,
  hashCode,
} from "@/lib/auth";
import {
  defaultSentence,
  isLang,
  serviceChoices,
  specialtyById,
  stageById,
  type Lang,
} from "@/lib/catalog";
import { getDb } from "@/lib/db";
import { isDay, mobileKey, tehranLocalInputToIso } from "@/lib/format";
import { buildSmsText, codeTtlMs, deliverSms, patientUrl } from "@/lib/sms";

export type PathDateInput = { title: string; eventDate: string };

export type CaseInput = {
  firstName: string;
  lastName: string;
  mobile: string;
  mobileKey: string;
  language: Lang;
  country: string;
  city: string;
  groupId: string;
  specialtyId: string;
  requested: string[];
  received: string[];
  centerName: string;
  summary: string;
  arrived: string | null;
  visitedCompany: string | null;
  visitedCenter: string | null;
  departed: string | null;
  internalNote: string;
  pathDates: PathDateInput[];
};

export type ActionResult = { ok: true; message: string } | { ok: false; message: string };

type CaseRow = {
  id: number;
  case_number: string;
  public_token: string;
  first_name: string;
  last_name: string;
  mobile: string;
  mobile_key: string;
  language: Lang;
  country: string;
  city: string;
  group_id: string;
  specialty_id: string;
  requested_services: string;
  received_services: string;
  center_name: string;
  summary: string;
  arrived_iran_on: string | null;
  visited_company_on: string | null;
  visited_center_on: string | null;
  departed_iran_on: string | null;
  internal_note: string;
  sms_text: string;
  sms_status: "unsent" | "sent" | "failed";
  sms_error: string | null;
  sms_attempted_at: string | null;
  code_failures: number;
  created_at: string;
  updated_at: string;
  current_stage_id?: string | null;
};

export type CaseEvent = {
  id: number;
  stage_id: string;
  sentence_override: string | null;
  happened_at: string;
};

export type CaseDetail = CaseRow & {
  requested: string[];
  received: string[];
  pathDates: PathDateInput[];
  events: CaseEvent[];
};

export type CaseFilters = {
  q: string;
  groupId: string;
  specialtyId: string;
  stageId: string;
  language: string;
  arrivedFrom: string;
  arrivedTo: string;
  departedFrom: string;
  departedTo: string;
};

function str(form: FormData, key: string) {
  const value = form.get(key);
  return typeof value === "string" ? value.trim() : "";
}

function optionalDay(value: string): { ok: true; value: string | null } | { ok: false } {
  if (!value) return { ok: true, value: null };
  if (!isDay(value)) return { ok: false };
  return { ok: true, value };
}

export function parseCaseForm(form: FormData): { ok: true; value: CaseInput } | { ok: false; message: string } {
  const firstName = str(form, "firstName");
  const lastName = str(form, "lastName");
  const mobile = str(form, "mobile");
  const language = str(form, "language");
  const country = str(form, "country");
  const city = str(form, "city");
  const specialtyId = str(form, "specialtyId");
  const centerName = str(form, "centerName");
  const summary = str(form, "summary");
  const internalNote = str(form, "internalNote");
  const requested = form.getAll("requested").map(String).filter(Boolean);
  const received = form.getAll("received").map(String).filter(Boolean);

  if (!firstName || firstName.length > 80) return { ok: false, message: "نام را وارد کنید." };
  if (!lastName || lastName.length > 80) return { ok: false, message: "نام خانوادگی را وارد کنید." };
  const key = mobileKey(mobile);
  if (!mobile || key.length < 8 || key.length > 15) return { ok: false, message: "موبایل را درست وارد کنید." };
  if (!isLang(language)) return { ok: false, message: "زبان پرونده درست نیست." };
  if (!country || country.length > 80 || !city || city.length > 80) {
    return { ok: false, message: "کشور و شهر محل سکونت را وارد کنید." };
  }
  const specialty = specialtyById(specialtyId);
  if (!specialty) return { ok: false, message: "تخصص را انتخاب کنید." };
  const allowed = new Set(serviceChoices(specialty).map((item) => item.id));
  const uniqueRequested = [...new Set(requested)];
  const uniqueReceived = [...new Set(received)];
  if (uniqueRequested.length === 0) return { ok: false, message: "حداقل یک خدمت درخواستی لازم است." };
  if (uniqueRequested.some((id) => !allowed.has(id)) || uniqueReceived.some((id) => !allowed.has(id))) {
    return { ok: false, message: "خدمت انتخاب‌شده جزء این تخصص نیست." };
  }
  if (centerName.length > 200 || summary.length > 2000 || internalNote.length > 5000) {
    return { ok: false, message: "یکی از متن‌ها بیش از حد بلند است." };
  }

  const arrived = optionalDay(str(form, "arrived"));
  const visitedCompany = optionalDay(str(form, "visitedCompany"));
  const visitedCenter = optionalDay(str(form, "visitedCenter"));
  const departed = optionalDay(str(form, "departed"));
  if (!arrived.ok || !visitedCompany.ok || !visitedCenter.ok || !departed.ok) {
    return { ok: false, message: "یکی از تاریخ‌ها درست نیست." };
  }

  const titles = form.getAll("pathTitle").map((item) => String(item).trim());
  const dates = form.getAll("pathDate").map((item) => String(item).trim());
  if (titles.length !== dates.length || titles.length > 30) {
    return { ok: false, message: "تاریخ‌های مسیر را دوباره وارد کنید." };
  }
  const pathDates: PathDateInput[] = [];
  for (let index = 0; index < titles.length; index += 1) {
    if (!titles[index] && !dates[index]) continue;
    if (!titles[index] || titles[index].length > 80 || !isDay(dates[index])) {
      return { ok: false, message: "عنوان تاریخ مسیر را همراه یک تاریخ درست بنویسید." };
    }
    pathDates.push({ title: titles[index], eventDate: dates[index] });
  }

  return {
    ok: true,
    value: {
      firstName,
      lastName,
      mobile,
      mobileKey: key,
      language,
      country,
      city,
      groupId: specialty.groupId,
      specialtyId,
      requested: uniqueRequested,
      received: uniqueReceived,
      centerName,
      summary,
      arrived: arrived.value,
      visitedCompany: visitedCompany.value,
      visitedCenter: visitedCenter.value,
      departed: departed.value,
      internalNote,
      pathDates,
    },
  };
}

function hydrate(row: CaseRow): CaseDetail {
  const db = getDb();
  const pathDates = db
    .prepare(
      "SELECT title, event_date AS eventDate FROM path_dates WHERE case_id = ? ORDER BY sort_order, id",
    )
    .all(row.id) as PathDateInput[];
  const events = db
    .prepare(
      "SELECT id, stage_id, sentence_override, happened_at FROM timeline_events WHERE case_id = ? ORDER BY happened_at, id",
    )
    .all(row.id) as CaseEvent[];
  return {
    ...row,
    language: isLang(row.language) ? row.language : "fa",
    requested: JSON.parse(row.requested_services) as string[],
    received: JSON.parse(row.received_services) as string[],
    pathDates,
    events,
  };
}

export function getCase(id: number) {
  const row = getDb().prepare("SELECT * FROM cases WHERE id = ?").get(id) as CaseRow | undefined;
  return row ? hydrate(row) : null;
}

export function getCaseByToken(token: string) {
  const row = getDb()
    .prepare("SELECT * FROM cases WHERE public_token = ?")
    .get(token) as CaseRow | undefined;
  return row ? hydrate(row) : null;
}

export function listByMobile(key: string) {
  const rows = getDb()
    .prepare(
      `SELECT c.*,
        (SELECT stage_id FROM timeline_events e
          WHERE e.case_id = c.id
          ORDER BY e.happened_at DESC, e.id DESC LIMIT 1) AS current_stage_id
       FROM cases c
       WHERE c.mobile_key = ?
       ORDER BY c.updated_at DESC`,
    )
    .all(key) as CaseRow[];
  return rows.map((row) => hydrate(row));
}

export type SearchRow = CaseRow & { current_stage_id: string | null };

export function searchCases(filters: CaseFilters) {
  const where: string[] = [];
  const params: unknown[] = [];
  if (filters.q) {
    const like = `%${filters.q.replace(/[%_]/g, "")}%`;
    where.push(
      "(first_name LIKE ? OR last_name LIKE ? OR (first_name || ' ' || last_name) LIKE ? OR mobile LIKE ? OR mobile_key LIKE ? OR case_number LIKE ?)",
    );
    params.push(like, like, like, like, `%${mobileKey(filters.q)}%`, like);
  }
  if (filters.groupId) {
    where.push("group_id = ?");
    params.push(filters.groupId);
  }
  if (filters.specialtyId) {
    where.push("specialty_id = ?");
    params.push(filters.specialtyId);
  }
  if (filters.stageId) {
    where.push("current_stage_id = ?");
    params.push(filters.stageId);
  }
  if (filters.language) {
    where.push("language = ?");
    params.push(filters.language);
  }
  if (filters.arrivedFrom) {
    where.push("arrived_iran_on IS NOT NULL AND arrived_iran_on >= ?");
    params.push(filters.arrivedFrom);
  }
  if (filters.arrivedTo) {
    where.push("arrived_iran_on IS NOT NULL AND arrived_iran_on <= ?");
    params.push(filters.arrivedTo);
  }
  if (filters.departedFrom) {
    where.push("departed_iran_on IS NOT NULL AND departed_iran_on >= ?");
    params.push(filters.departedFrom);
  }
  if (filters.departedTo) {
    where.push("departed_iran_on IS NOT NULL AND departed_iran_on <= ?");
    params.push(filters.departedTo);
  }
  const sql = `
    SELECT * FROM (
      SELECT c.*,
        (SELECT stage_id FROM timeline_events e
          WHERE e.case_id = c.id
          ORDER BY e.happened_at DESC, e.id DESC LIMIT 1) AS current_stage_id
      FROM cases c
    )
    ${where.length ? `WHERE ${where.join(" AND ")}` : ""}
    ORDER BY updated_at DESC
  `;
  return getDb().prepare(sql).all(...params) as SearchRow[];
}

function writePathDates(caseId: number, dates: PathDateInput[]) {
  const db = getDb();
  db.prepare("DELETE FROM path_dates WHERE case_id = ?").run(caseId);
  const insert = db.prepare(
    "INSERT INTO path_dates (case_id, title, event_date, sort_order) VALUES (?, ?, ?, ?)",
  );
  dates.forEach((item, index) => insert.run(caseId, item.title, item.eventDate, index));
}

function nextCaseNumber() {
  const row = getDb()
    .prepare("SELECT case_number FROM cases ORDER BY id DESC LIMIT 1")
    .get() as { case_number: string } | undefined;
  const current = row ? Number(row.case_number.replace(/\D/g, "")) : 10000;
  return `IH-${String(current + 1).padStart(5, "0")}`;
}

function insertCode(caseId: number, visible: boolean) {
  const plain = generateCode();
  const now = new Date();
  const expires = new Date(now.getTime() + codeTtlMs()).toISOString();
  const info = getDb()
    .prepare(
      `INSERT INTO patient_codes
        (case_id, code_hash, code_plain, visible_to_staff, expires_at, created_at)
       VALUES (?, ?, ?, ?, ?, ?)`,
    )
    .run(caseId, hashCode(plain), visible ? plain : null, visible ? 1 : 0, expires, now.toISOString());
  return { id: Number(info.lastInsertRowid), plain, expires };
}

export function createCaseRecord(input: CaseInput) {
  const db = getDb();
  const now = new Date().toISOString();
  const token = generateToken();
  const smsText = buildSmsText(input.language, patientUrl(token));
  const visibleAtCreate = !smsConfiguredSafe();
  const run = db.transaction(() => {
    const info = db
      .prepare(
        `INSERT INTO cases (
          case_number, public_token, first_name, last_name, mobile, mobile_key, language,
          country, city, group_id, specialty_id, requested_services, received_services,
          center_name, summary, arrived_iran_on, visited_company_on, visited_center_on,
          departed_iran_on, internal_note, sms_text, sms_status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unsent', ?, ?)`,
      )
      .run(
        nextCaseNumber(),
        token,
        input.firstName,
        input.lastName,
        input.mobile,
        input.mobileKey,
        input.language,
        input.country,
        input.city,
        input.groupId,
        input.specialtyId,
        JSON.stringify(input.requested),
        JSON.stringify(input.received),
        input.centerName,
        input.summary,
        input.arrived,
        input.visitedCompany,
        input.visitedCenter,
        input.departed,
        input.internalNote,
        smsText,
        now,
        now,
      );
    const id = Number(info.lastInsertRowid);
    writePathDates(id, input.pathDates);
    db.prepare(
      "INSERT INTO timeline_events (case_id, stage_id, sentence_override, happened_at) VALUES (?, 'file-created', NULL, ?)",
    ).run(id, now);
    const code = insertCode(id, visibleAtCreate);
    return { id, codeId: code.id };
  });
  return run();
}

function smsConfiguredSafe() {
  return Boolean(
    process.env.SMS_API_URL?.trim() &&
      process.env.SMS_API_KEY?.trim() &&
      process.env.SMS_FROM?.trim(),
  );
}

export async function sendStoredSms(caseId: number) {
  const row = getDb()
    .prepare("SELECT id, mobile, sms_text FROM cases WHERE id = ?")
    .get(caseId) as { id: number; mobile: string; sms_text: string } | undefined;
  if (!row) return;
  const result = await deliverSms(row.mobile, row.sms_text);
  const now = new Date().toISOString();
  if (result.status === "unsent") return;
  getDb()
    .prepare(
      "UPDATE cases SET sms_status = ?, sms_error = ?, sms_attempted_at = ?, updated_at = ? WHERE id = ?",
    )
    .run(result.status, result.status === "failed" ? result.error : null, now, now, caseId);
  if (result.status === "failed") issueCode(caseId, true);
}

export function updateCaseRecord(id: number, input: CaseInput) {
  const db = getDb();
  const now = new Date().toISOString();
  const run = db.transaction(() => {
    db.prepare(
      `UPDATE cases SET
        first_name = ?, last_name = ?, mobile = ?, mobile_key = ?, language = ?,
        country = ?, city = ?, group_id = ?, specialty_id = ?,
        requested_services = ?, received_services = ?, center_name = ?, summary = ?,
        arrived_iran_on = ?, visited_company_on = ?, visited_center_on = ?,
        departed_iran_on = ?, internal_note = ?, updated_at = ?
       WHERE id = ?`,
    ).run(
      input.firstName,
      input.lastName,
      input.mobile,
      input.mobileKey,
      input.language,
      input.country,
      input.city,
      input.groupId,
      input.specialtyId,
      JSON.stringify(input.requested),
      JSON.stringify(input.received),
      input.centerName,
      input.summary,
      input.arrived,
      input.visitedCompany,
      input.visitedCenter,
      input.departed,
      input.internalNote,
      now,
      id,
    );
    writePathDates(id, input.pathDates);
  });
  run();
}

export function addTimelineEvent(caseId: number, stageId: string, sentence: string | null, happenedAt: string) {
  const db = getDb();
  db.prepare(
    "INSERT INTO timeline_events (case_id, stage_id, sentence_override, happened_at) VALUES (?, ?, ?, ?)",
  ).run(caseId, stageId, sentence, happenedAt);
  db.prepare("UPDATE cases SET updated_at = ? WHERE id = ?").run(new Date().toISOString(), caseId);
}

export function latestCode(caseId: number) {
  return getDb()
    .prepare(
      `SELECT id, code_plain, visible_to_staff, expires_at, used_at, created_at
       FROM patient_codes WHERE case_id = ? ORDER BY id DESC LIMIT 1`,
    )
    .get(caseId) as
    | {
        id: number;
        code_plain: string | null;
        visible_to_staff: number;
        expires_at: string;
        used_at: string | null;
        created_at: string;
      }
    | undefined;
}

export function issueCode(caseId: number, visible: boolean) {
  const db = getDb();
  const now = new Date().toISOString();
  const run = db.transaction(() => {
    db.prepare(
      "UPDATE patient_codes SET used_at = ?, code_plain = NULL WHERE case_id = ? AND used_at IS NULL",
    ).run(now, caseId);
    db.prepare("UPDATE cases SET code_failures = 0, updated_at = ? WHERE id = ?").run(now, caseId);
    return insertCode(caseId, visible);
  });
  return run();
}

function keysMatch(left: string, right: string) {
  const a = Buffer.from(left);
  const b = Buffer.from(right);
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

export function verifyPatientCode(token: string, mobile: string, code: string) {
  const row = getDb()
    .prepare("SELECT id, mobile_key, code_failures FROM cases WHERE public_token = ?")
    .get(token) as { id: number; mobile_key: string; code_failures: number } | undefined;
  if (!row) return { ok: false as const, reason: "invalid" as const };
  if (row.code_failures >= 8) return { ok: false as const, reason: "locked" as const };
  const codeRow = getDb()
    .prepare(
      `SELECT id, code_hash, expires_at FROM patient_codes
       WHERE case_id = ? AND used_at IS NULL
       ORDER BY id DESC LIMIT 1`,
    )
    .get(row.id) as { id: number; code_hash: string; expires_at: string } | undefined;
  const now = new Date().toISOString();
  const mobileOk = keysMatch(mobileKey(mobile), row.mobile_key);
  const codeOk = Boolean(
    codeRow && codeRow.expires_at > now && checkCode(code.trim(), codeRow.code_hash),
  );
  if (!mobileOk || !codeOk || !codeRow) {
    getDb().prepare("UPDATE cases SET code_failures = code_failures + 1 WHERE id = ?").run(row.id);
    if (row.code_failures + 1 >= 8) return { ok: false as const, reason: "locked" as const };
    return { ok: false as const, reason: "invalid" as const };
  }
  getDb()
    .prepare("UPDATE patient_codes SET used_at = ?, code_plain = NULL WHERE id = ?")
    .run(now, codeRow.id);
  getDb().prepare("UPDATE cases SET code_failures = 0 WHERE id = ?").run(row.id);
  return { ok: true as const, mobileKey: row.mobile_key };
}

export function setCaseLanguage(id: number, language: Lang) {
  getDb()
    .prepare("UPDATE cases SET language = ?, updated_at = ? WHERE id = ?")
    .run(language, new Date().toISOString(), id);
}

export function normalizeSentence(stageId: string, language: Lang, raw: string) {
  const trimmed = raw.trim();
  if (!trimmed) return null;
  if (trimmed === defaultSentence(stageId, language)) return null;
  return trimmed.slice(0, 500);
}

export function parseEventTime(raw: string) {
  if (!raw) return new Date().toISOString();
  return tehranLocalInputToIso(raw);
}

export function stageExists(stageId: string) {
  return Boolean(stageById(stageId));
}
