/**
 * Phiếu đề xuất — mở lớp chuyển giao / khuyến mãi / quà tặng / khác (yêu cầu mục Đề xuất).
 *
 * Quy trình duyệt LẦN LƯỢT: Người đề xuất → Trưởng bộ phận → Trưởng bộ phận liên quan → TGĐ. Mỗi người duyệt thuộc một BƯỚC
 * (`proposal_approvers.sort_order`); chỉ đến lượt khi mọi bước trước đã duyệt. Cùng bước thì duyệt song song (nhiều trưởng bộ phận liên quan).
 * Một người từ chối là chốt từ chối cả phiếu; đủ mọi bước duyệt mới xong.
 * Ai xem được phiếu: người đề xuất, những người có tên trong chuỗi duyệt, và Ban giám đốc (xem toàn bộ).
 */
import { and, asc, desc, eq, ilike, inArray, or, sql, type SQL } from "drizzle-orm";
import { db } from "@/db";
import { departments, proposals, proposalApprovers, users } from "@/db/schema";
import type { SessionUser } from "./auth";
import { PROPOSAL_TYPES, STEP_LABEL, type ProposalDetails, type ProposalTypeKey } from "./proposal-types";

export const PROPOSAL_PAGE_SIZE = 20;

export type Decision = "PENDING" | "APPROVED" | "REJECTED";

export type ProposalApproverRow = {
  id: string;
  approverId: string;
  approverName: string;
  decision: Decision;
  note: string | null;
  decidedAt: Date | null;
  step: number;
  stepLabel: string | null;
  /** Đến lượt duyệt: chưa quyết định, phiếu còn mở và mọi bước trước đã duyệt. */
  isTurn: boolean;
};

export type ProposalRow = {
  id: string;
  title: string;
  kind: string | null;
  content: string;
  status: Decision;
  createdBy: string;
  createdByName: string;
  createdAt: Date;
  decidedAt: Date | null;
  details: ProposalDetails | null;
  approvers: ProposalApproverRow[];
  /** Người xem đang là người phải duyệt ở bước hiện tại. */
  myTurnRowId: string | null;
  /** Người đề xuất được thu hồi khi chưa ai duyệt/từ chối. */
  canWithdraw: boolean;
};

/** duyet = đến lượt tôi duyệt; toi = tôi đề xuất; rỗng = tất cả phiếu tôi xem được (Ban giám đốc: cả công ty). */
export type ProposalView = "duyet" | "toi" | "";

export type ProposalQuery = {
  view: ProposalView;
  status: "" | Decision;
  type: "" | ProposalTypeKey;
  q: string;
  page: number;
};

export function parseProposalQuery(get: (key: string) => string | undefined | null): ProposalQuery {
  const view = get("xem");
  const status = get("tt");
  const type = get("loai");
  const page = Number(get("trang"));
  return {
    view: (view === "duyet" || view === "toi" ? view : "") as ProposalView,
    status: (["PENDING", "APPROVED", "REJECTED"].includes(status ?? "") ? status : "") as ProposalQuery["status"],
    type: (PROPOSAL_TYPES.some((t) => t.key === type) ? type : "") as ProposalQuery["type"],
    q: (get("q") ?? "").trim().slice(0, 100),
    page: Number.isInteger(page) && page >= 1 && page <= 10_000 ? page : 1,
  };
}

/** Điều kiện SQL "đến lượt tôi duyệt" — dùng tên bảng tường minh để không dính lỗi cột `id` trần của Drizzle trong truy vấn con. */
function myTurnCondition(userId: string): SQL {
  return sql`exists (
    select 1 from proposal_approvers pa
    where pa.proposal_id = proposals.id
      and pa.approver_id = ${userId}
      and pa.decision = 'PENDING'
      and proposals.status = 'PENDING'
      and not exists (
        select 1 from proposal_approvers p2
        where p2.proposal_id = pa.proposal_id and p2.sort_order < pa.sort_order and p2.decision <> 'APPROVED'
      )
  )`;
}

function viewCondition(session: SessionUser, view: ProposalView): SQL | undefined {
  if (view === "duyet") return myTurnCondition(session.userId);
  if (view === "toi") return eq(proposals.createdBy, session.userId);
  return undefined;
}

/** Phạm vi xem: người đề xuất, người có tên trong chuỗi duyệt, hoặc Ban giám đốc (thấy tất cả). */
function visibleCondition(session: SessionUser): SQL | undefined {
  if (session.role === "DIRECTOR") return undefined;
  return or(
    eq(proposals.createdBy, session.userId),
    sql`exists (select 1 from proposal_approvers pa where pa.proposal_id = proposals.id and pa.approver_id = ${session.userId})`,
  );
}

export async function loadProposals(session: SessionUser, query: ProposalQuery) {
  const base: (SQL | undefined)[] = [eq(proposals.tenantId, session.tenantId), visibleCondition(session)];
  const filters: (SQL | undefined)[] = [
    viewCondition(session, query.view),
    query.status ? eq(proposals.status, query.status) : undefined,
    query.type ? sql`${proposals.details}->>'type' = ${query.type}` : undefined,
    query.q ? or(ilike(proposals.title, `%${query.q}%`), ilike(proposals.content, `%${query.q}%`)) : undefined,
  ];
  const where = and(...base, ...filters);

  const [{ total }] = await db.select({ total: sql<number>`count(*)::int` }).from(proposals).where(where);
  const totalPages = Math.max(1, Math.ceil(total / PROPOSAL_PAGE_SIZE));
  const page = Math.min(query.page, totalPages);

  const rows = await db
    .select({
      id: proposals.id,
      title: proposals.title,
      kind: proposals.kind,
      content: proposals.content,
      status: proposals.status,
      details: proposals.details,
      createdBy: proposals.createdBy,
      createdAt: proposals.createdAt,
      decidedAt: proposals.decidedAt,
      createdByName: users.fullName,
    })
    .from(proposals)
    .innerJoin(users, eq(proposals.createdBy, users.id))
    .where(where)
    .orderBy(desc(proposals.createdAt))
    .limit(PROPOSAL_PAGE_SIZE)
    .offset((page - 1) * PROPOSAL_PAGE_SIZE);

  const approverRows =
    rows.length > 0
      ? await db
          .select({
            id: proposalApprovers.id,
            proposalId: proposalApprovers.proposalId,
            approverId: proposalApprovers.approverId,
            decision: proposalApprovers.decision,
            note: proposalApprovers.note,
            decidedAt: proposalApprovers.decidedAt,
            step: proposalApprovers.sortOrder,
            stepLabel: proposalApprovers.stepLabel,
            approverName: users.fullName,
          })
          .from(proposalApprovers)
          .innerJoin(users, eq(proposalApprovers.approverId, users.id))
          .where(
            and(
              eq(proposalApprovers.tenantId, session.tenantId),
              inArray(
                proposalApprovers.proposalId,
                rows.map((r) => r.id),
              ),
            ),
          )
          .orderBy(asc(proposalApprovers.sortOrder), asc(users.fullName))
      : [];

  const byProposal = new Map<string, typeof approverRows>();
  for (const row of approverRows) byProposal.set(row.proposalId, [...(byProposal.get(row.proposalId) ?? []), row]);

  const result: ProposalRow[] = rows.map((row) => {
    const list = byProposal.get(row.id) ?? [];
    const approvers: ProposalApproverRow[] = list.map((a) => ({
      id: a.id,
      approverId: a.approverId,
      approverName: a.approverName,
      decision: a.decision,
      note: a.note,
      decidedAt: a.decidedAt,
      step: a.step,
      stepLabel: a.stepLabel,
      isTurn: row.status === "PENDING" && a.decision === "PENDING" && list.every((o) => o.step >= a.step || o.decision === "APPROVED"),
    }));
    return {
      ...row,
      details: (row.details as ProposalDetails | null) ?? null,
      approvers,
      myTurnRowId: approvers.find((a) => a.approverId === session.userId && a.isTurn)?.id ?? null,
      canWithdraw: row.createdBy === session.userId && row.status === "PENDING" && approvers.every((a) => a.decision === "PENDING"),
    };
  });

  return { rows: result, total, page, totalPages, counts: await proposalCounts(session) };
}

/** Số phiếu ở từng chế độ xem — cho các tab (tính trên toàn bộ phiếu xem được, không theo bộ lọc). */
export async function proposalCounts(session: SessionUser): Promise<{ duyet: number; toi: number; all: number }> {
  const visible = and(eq(proposals.tenantId, session.tenantId), visibleCondition(session));
  const countOf = async (view: ProposalView) => {
    const [row] = await db
      .select({ n: sql<number>`count(*)::int` })
      .from(proposals)
      .where(and(visible, viewCondition(session, view)));
    return Number(row?.n ?? 0);
  };
  const [duyet, toi, all] = await Promise.all([countOf("duyet"), countOf("toi"), countOf("")]);
  return { duyet, toi, all };
}

/** Suy ra trạng thái tổng từ trạng thái từng người duyệt: 1 từ chối là chốt từ chối, đủ người duyệt mới coi là xong. */
export function computeProposalStatus(decisions: Decision[]): Decision {
  if (decisions.some((d) => d === "REJECTED")) return "REJECTED";
  if (decisions.length > 0 && decisions.every((d) => d === "APPROVED")) return "APPROVED";
  return "PENDING";
}

/* -------------------------------------------------------------------------- */
/* Chuỗi duyệt                                                                */
/* -------------------------------------------------------------------------- */

export type ChainOptions = {
  /** Trưởng bộ phận trực tiếp của người đề xuất (tự động), hoặc null nếu không xác định được. */
  head: { id: string; name: string; departmentName: string } | null;
  /** Các bộ phận có trưởng bộ phận — để chọn "Trưởng bộ phận liên quan". */
  departments: { id: string; name: string; headId: string; headName: string }[];
  /** Người có vai trò TGĐ (Ban giám đốc). */
  directors: { id: string; name: string }[];
  /** Đồng nghiệp để gắn thêm làm người duyệt (tuỳ chọn). */
  colleagues: { id: string; name: string }[];
};

/** Trưởng bộ phận trực tiếp: đi từ phòng ban của người đề xuất lên các cấp cha, bỏ qua các phòng mà chính người đề xuất là trưởng. */
async function findHead(session: SessionUser) {
  const [me] = await db
    .select({ departmentId: users.departmentId, role: users.role })
    .from(users)
    .where(and(eq(users.tenantId, session.tenantId), eq(users.id, session.userId)))
    .limit(1);
  if (!me?.departmentId) return null;

  const [deptRows, activeUsers] = await Promise.all([
    db
      .select({ id: departments.id, name: departments.name, parentId: departments.parentId, headUserId: departments.headUserId })
      .from(departments)
      .where(and(eq(departments.tenantId, session.tenantId), eq(departments.isActive, true))),
    db
      .select({ id: users.id, name: users.fullName })
      .from(users)
      .where(and(eq(users.tenantId, session.tenantId), eq(users.isActive, true))),
  ]);
  const deptById = new Map(deptRows.map((d) => [d.id, d]));
  const userById = new Map(activeUsers.map((u) => [u.id, u.name]));

  let cursor: string | null = me.departmentId;
  const seen = new Set<string>();
  while (cursor && !seen.has(cursor)) {
    seen.add(cursor);
    const dept = deptById.get(cursor);
    if (!dept) break;
    if (dept.headUserId && dept.headUserId !== session.userId && userById.has(dept.headUserId)) {
      return { id: dept.headUserId, name: userById.get(dept.headUserId)!, departmentName: dept.name };
    }
    cursor = dept.parentId;
  }
  return null;
}

export async function chainOptions(session: SessionUser): Promise<ChainOptions> {
  const [head, deptRows, directorRows, colleagueRows] = await Promise.all([
    findHead(session),
    db
      .select({ id: departments.id, name: departments.name, headId: departments.headUserId, headName: users.fullName })
      .from(departments)
      .innerJoin(users, and(eq(departments.headUserId, users.id), eq(users.isActive, true)))
      .where(and(eq(departments.tenantId, session.tenantId), eq(departments.isActive, true)))
      .orderBy(asc(departments.sortOrder), asc(departments.name)),
    db
      .select({ id: users.id, name: users.fullName })
      .from(users)
      .where(and(eq(users.tenantId, session.tenantId), eq(users.isActive, true), eq(users.role, "DIRECTOR")))
      .orderBy(asc(users.fullName)),
    db
      .select({ id: users.id, name: users.fullName })
      .from(users)
      .where(and(eq(users.tenantId, session.tenantId), eq(users.isActive, true)))
      .orderBy(asc(users.fullName)),
  ]);
  return {
    head,
    departments: deptRows.filter((d): d is typeof d & { headId: string } => !!d.headId).map((d) => ({ id: d.id, name: d.name, headId: d.headId, headName: d.headName })),
    directors: directorRows.filter((d) => d.id !== session.userId),
    colleagues: colleagueRows.filter((c) => c.id !== session.userId),
  };
}

export type ChainStep = { label: string; userIds: string[] };

/**
 * Dựng chuỗi duyệt từ lựa chọn của người đề xuất — kiểm lại ở máy chủ (người duyệt phải là nhân viên đang hoạt động cùng công ty):
 *   1. Trưởng bộ phận (tự động)  2. Trưởng bộ phận liên quan (chọn bộ phận)  3. Người duyệt thêm (tuỳ chọn)  4. TGĐ.
 * Một người chỉ xuất hiện ở bước đầu tiên của họ; người đề xuất không tự duyệt phiếu của mình.
 */
export async function resolveChain(
  session: SessionUser,
  input: { relatedDepartmentIds: string[]; extraApproverIds: string[]; directorId: string },
): Promise<{ steps: ChainStep[]; error?: string }> {
  const options = await chainOptions(session);
  const steps: ChainStep[] = [];
  const used = new Set<string>([session.userId]);
  const push = (label: string, ids: string[]) => {
    const fresh = [...new Set(ids)].filter((id) => !used.has(id));
    if (fresh.length === 0) return;
    fresh.forEach((id) => used.add(id));
    steps.push({ label, userIds: fresh });
  };

  if (options.head) push(STEP_LABEL.head, [options.head.id]);

  const relatedHeads: string[] = [];
  for (const departmentId of input.relatedDepartmentIds) {
    const dept = options.departments.find((d) => d.id === departmentId);
    if (!dept) return { steps: [], error: "Bộ phận liên quan không hợp lệ hoặc chưa có trưởng bộ phận." };
    relatedHeads.push(dept.headId);
  }
  push(STEP_LABEL.related, relatedHeads);

  const knownColleagues = new Set(options.colleagues.map((c) => c.id));
  if (input.extraApproverIds.some((id) => !knownColleagues.has(id))) return { steps: [], error: "Người duyệt thêm không hợp lệ." };
  push(STEP_LABEL.extra, input.extraApproverIds);

  // Phiếu phải đi qua TGĐ (nếu công ty có và người đề xuất không phải TGĐ) — theo yêu cầu chuỗi duyệt.
  if (options.directors.length > 0 && !input.directorId) return { steps: [], error: "Chọn TGĐ duyệt phiếu." };
  if (input.directorId) {
    if (!options.directors.some((d) => d.id === input.directorId)) return { steps: [], error: "TGĐ được chọn không hợp lệ." };
    push(STEP_LABEL.director, [input.directorId]);
  }

  if (steps.length === 0) return { steps: [], error: "Chưa có người duyệt nào — chọn TGĐ hoặc bộ phận liên quan." };
  return { steps };
}
