import { FastifyInstance } from "fastify";
import { z } from "zod";
import { nanoid } from "nanoid";
import { db } from "./db.js";
import { requireAdmin, requireAuth } from "./auth.js";
import { getFolder, modelPermissions, requireFolderWrite, requireModelWrite } from "./permissions.js";
import { deleteStoredObject, publicObjectUrl, storage } from "./storage.js";
import { DictionaryRow, FolderRow, ModelRow } from "./types.js";
import {
  assertGlbFilename,
  assertValidFolderName,
  normalizeModelName,
  toStorageRelative
} from "./utils.js";

const updateSchema = z.object({
  name: z.string().min(1),
  brandName: z.string().optional(),
  typeName: z.string().optional(),
  operationTree: z.string().optional(),
  properties: z.record(z.string()).optional()
});

const thumbnailSchema = z.object({
  thumbnail: z.string().startsWith("data:image/").max(2_000_000)
});

const dictionaryKindSchema = z.object({
  kind: z.enum(["brands", "types"])
});

const dictionaryBodySchema = z.object({
  name: z.string().min(1).max(80)
});

const defaultOperationTree = JSON.stringify({ OperationTree: "[]" });

function getProperties(modelId: number) {
  const rows = db.prepare("SELECT property_key, property_value FROM model_properties WHERE model_id = ?")
    .all(modelId) as { property_key: string; property_value: string }[];
  return Object.fromEntries(rows.map((row) => [row.property_key, row.property_value]));
}

function getPropertiesMap(modelIds: number[]) {
  const properties = new Map<number, Record<string, string>>();
  if (modelIds.length === 0) return properties;
  const placeholders = modelIds.map(() => "?").join(",");
  const rows = db.prepare(`
    SELECT model_id, property_key, property_value
    FROM model_properties
    WHERE model_id IN (${placeholders})
  `).all(...modelIds) as { model_id: number; property_key: string; property_value: string }[];

  for (const row of rows) {
    const modelProperties = properties.get(row.model_id) ?? {};
    modelProperties[row.property_key] = row.property_value;
    properties.set(row.model_id, modelProperties);
  }
  return properties;
}

function saveProperties(modelId: number, properties: Record<string, string>) {
  const stmt = db.prepare(`
    INSERT INTO model_properties (model_id, property_key, property_value)
    VALUES (?, ?, ?)
    ON CONFLICT(model_id, property_key)
    DO UPDATE SET property_value = excluded.property_value
  `);
  for (const [key, value] of Object.entries(properties)) {
    const cleanKey = key.trim();
    if (!cleanKey) continue;
    stmt.run(modelId, cleanKey, String(value ?? ""));
  }
}

function modelPayload(row: ModelRow, userInfo?: { id: number; username: string; role: "admin" | "user" }) {
  return {
    ...row,
    file_url: publicObjectUrl(row.file_path, row.storage_provider),
    thumbnail_url: row.thumbnail_path && row.thumbnail_provider
      ? publicObjectUrl(row.thumbnail_path, row.thumbnail_provider)
      : null,
    permissions: userInfo ? modelPermissions(userInfo, row) : { read: true, write: false },
    properties: getProperties(row.id)
  };
}

function modelListPayload(
  rows: ModelRow[],
  userInfo?: { id: number; username: string; role: "admin" | "user" }
) {
  const propertiesMap = getPropertiesMap(rows.map((row) => row.id));
  return rows.map((row) => ({
    ...row,
    file_url: publicObjectUrl(row.file_path, row.storage_provider),
    thumbnail_url: row.thumbnail_path && row.thumbnail_provider
      ? publicObjectUrl(row.thumbnail_path, row.thumbnail_provider)
      : null,
    permissions: userInfo ? modelPermissions(userInfo, row) : { read: true, write: false },
    properties: propertiesMap.get(row.id) ?? {}
  }));
}

function dataImageToBuffer(dataUrl: string) {
  const match = dataUrl.match(/^data:(image\/png|image\/jpeg|image\/webp);base64,(.+)$/);
  if (!match) {
    throw new Error("缩略图格式不正确");
  }
  return {
    ext: match[1] === "image/png" ? "png" : match[1] === "image/webp" ? "webp" : "jpg",
    buffer: Buffer.from(match[2], "base64")
  };
}

function normalizeOperationTree(value: string | undefined) {
  const clean = value?.trim();
  if (!clean) return defaultOperationTree;
  let parsed: unknown;
  try {
    parsed = JSON.parse(clean);
  } catch {
    throw new Error("工艺数据必须是合法 JSON 字符串");
  }
  if (!parsed || typeof parsed !== "object" || typeof (parsed as { OperationTree?: unknown }).OperationTree !== "string") {
    throw new Error("工艺数据格式必须符合 OperationTreeDB");
  }
  try {
    JSON.parse((parsed as { OperationTree: string }).OperationTree);
  } catch {
    throw new Error("OperationTree 字段必须是合法 JSON 字符串");
  }
  return JSON.stringify(parsed);
}

function normalizeDictionaryName(name: string | undefined) {
  const clean = name?.trim();
  return clean || null;
}

function findOrCreateDictionary(table: "brands" | "model_types", name: string | undefined) {
  const cleanName = normalizeDictionaryName(name);
  if (!cleanName) return null;
  const existing = db.prepare(`SELECT id FROM ${table} WHERE name = ?`).get(cleanName) as { id: number } | undefined;
  if (existing) return existing.id;
  const result = db.prepare(`INSERT INTO ${table} (name) VALUES (?)`).run(cleanName);
  return Number(result.lastInsertRowid);
}

function dictionaryList(table: "brands" | "model_types") {
  return db.prepare(`SELECT id, name FROM ${table} ORDER BY name`).all() as unknown as DictionaryRow[];
}

function dictionaryTable(kind: "brands" | "types") {
  return kind === "brands" ? "brands" : "model_types";
}

export async function modelRoutes(app: FastifyInstance) {
  app.get("/api/dictionaries", { preHandler: [requireAuth] }, async () => {
    return {
      brands: dictionaryList("brands"),
      types: dictionaryList("model_types")
    };
  });

  app.post("/api/dictionaries/:kind", { preHandler: [requireAdmin] }, async (request, reply) => {
    const params = dictionaryKindSchema.parse(request.params);
    const body = dictionaryBodySchema.parse(request.body);
    const name = body.name.trim();
    const table = dictionaryTable(params.kind);
    try {
      const result = db.prepare(`INSERT INTO ${table} (name) VALUES (?)`).run(name);
      return reply.code(201).send({ id: result.lastInsertRowid, name });
    } catch {
      return reply.code(409).send({ message: "名称已存在" });
    }
  });

  app.put("/api/dictionaries/:kind/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
    const params = dictionaryKindSchema.extend({ id: z.coerce.number().int() }).parse(request.params);
    const body = dictionaryBodySchema.parse(request.body);
    const name = body.name.trim();
    const table = dictionaryTable(params.kind);
    try {
      const result = db.prepare(`UPDATE ${table} SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`)
        .run(name, params.id);
      if (result.changes === 0) {
        return reply.code(404).send({ message: "字典项不存在" });
      }
      return { ok: true };
    } catch {
      return reply.code(409).send({ message: "名称已存在" });
    }
  });

  app.delete("/api/dictionaries/:kind/:id", { preHandler: [requireAdmin] }, async (request, reply) => {
    const params = dictionaryKindSchema.extend({ id: z.coerce.number().int() }).parse(request.params);
    const table = dictionaryTable(params.kind);
    const result = db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(params.id);
    if (result.changes === 0) {
      return reply.code(404).send({ message: "字典项不存在" });
    }
    return { ok: true };
  });

  app.get("/api/models", { preHandler: [requireAuth] }, async (request) => {
    const query = z.object({
      folderId: z.coerce.number().int().optional(),
      brandId: z.coerce.number().int().optional(),
      typeId: z.coerce.number().int().optional(),
      keyword: z.string().optional(),
      page: z.coerce.number().int().min(1).default(1),
      pageSize: z.coerce.number().int().min(1).max(100).default(12)
    }).parse(request.query);

    const offset = (query.page - 1) * query.pageSize;
    const whereParts: string[] = [];
    const params: Array<string | number> = [];
    if (query.folderId) {
      whereParts.push("m.folder_id = ?");
      params.push(query.folderId);
    }
    if (query.brandId) {
      whereParts.push("m.brand_id = ?");
      params.push(query.brandId);
    }
    if (query.typeId) {
      whereParts.push("m.type_id = ?");
      params.push(query.typeId);
    }
    const keyword = query.keyword?.trim() ?? "";
    const hasKeyword = Boolean(keyword);
    if (hasKeyword) {
      whereParts.push("(m.name LIKE ? OR m.original_filename LIKE ? OR mp.property_value LIKE ?)");
      const like = `%${keyword}%`;
      params.push(like, like, like);
    }
    const where = whereParts.length > 0 ? `WHERE ${whereParts.join(" AND ")}` : "";
    const propertyJoin = hasKeyword ? "LEFT JOIN model_properties mp ON mp.model_id = m.id" : "";
    const distinct = hasKeyword ? "DISTINCT" : "";
    const totalStmt = db.prepare(`
      SELECT COUNT(${distinct} m.id) as count
      FROM models m
      ${propertyJoin}
      ${where}
    `);
    const total = totalStmt.get(...params) as { count: number };
    const listStmt = db.prepare(`
      SELECT ${distinct}
        m.*,
        b.name AS brand_name,
        t.name AS type_name
      FROM models m
      LEFT JOIN brands b ON b.id = m.brand_id
      LEFT JOIN model_types t ON t.id = m.type_id
      ${propertyJoin}
      ${where}
      ORDER BY m.updated_at DESC, m.id DESC
      LIMIT ? OFFSET ?
    `);
    const rows = listStmt.all(...params, query.pageSize, offset) as unknown as ModelRow[];

    return {
      total: total.count,
      page: query.page,
      pageSize: query.pageSize,
      items: modelListPayload(rows, request.userInfo)
    };
  });

  app.post("/api/models/upload", { preHandler: [requireAuth] }, async (request, reply) => {
    const parts = request.parts();
    let folderId: number | undefined;
    const properties: Record<string, string> = {};
    let uploaded: { filename: string; buffer: Buffer } | undefined;
    let modelName: string | undefined;
    let brandName: string | undefined;
    let typeName: string | undefined;
    let operationTree: string | undefined;

    for await (const part of parts) {
      if (part.type === "file") {
        if (part.fieldname === "file") {
          const chunks = [];
          for await (const chunk of part.file) {
            chunks.push(chunk);
          }
          uploaded = { filename: part.filename, buffer: Buffer.concat(chunks) };
        }
        continue;
      }
      if (part.fieldname === "folderId") folderId = Number(part.value);
      if (part.fieldname === "name") modelName = String(part.value ?? "");
      if (part.fieldname === "brandName") brandName = String(part.value ?? "");
      if (part.fieldname === "typeName") typeName = String(part.value ?? "");
      if (part.fieldname === "operationTree") operationTree = String(part.value ?? "");
      if (part.fieldname.startsWith("prop.")) {
        properties[part.fieldname.slice(5)] = String(part.value ?? "");
      }
    }

    if (!folderId || !uploaded) {
      return reply.code(400).send({ message: "缺少目录或模型文件" });
    }
    assertGlbFilename(uploaded.filename);
    const folder = getFolder(folderId);
    if (!folder) {
      return reply.code(404).send({ message: "目录不存在" });
    }
    if (!requireFolderWrite(request, reply, folder)) return;

    const originalModelName = assertValidFolderName(modelName || normalizeModelName(uploaded.filename));
    const storageFilename = `${normalizeModelName(uploaded.filename)}-${nanoid(8)}.glb`;
    const objectKey = toStorageRelative(folder.path, storageFilename);

    const exists = db.prepare("SELECT id FROM models WHERE folder_id = ? AND name = ?").get(folderId, originalModelName);
    if (exists) {
      return reply.code(409).send({ message: "当前目录下模型名称已存在" });
    }

    const stored = await storage.putObject(objectKey, uploaded.buffer);
    try {
      const brandId = findOrCreateDictionary("brands", brandName);
      const typeId = findOrCreateDictionary("model_types", typeName);
      const normalizedOperationTree = normalizeOperationTree(operationTree);
      const result = db.prepare(`
        INSERT INTO models (folder_id, brand_id, type_id, name, original_filename, file_path, library_type, owner_user_id, storage_provider, file_size, operation_tree)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).run(folderId, brandId, typeId, originalModelName, uploaded.filename, stored.key, folder.library_type, folder.owner_user_id, stored.provider, uploaded.buffer.length, normalizedOperationTree);
      saveProperties(Number(result.lastInsertRowid), properties);
      return reply.code(201).send({ id: result.lastInsertRowid });
    } catch (error) {
      await storage.deleteObject(stored.key);
      throw error;
    }
  });

  app.put("/api/models/:id", { preHandler: [requireAuth] }, async (request, reply) => {
    const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
    const body = updateSchema.parse(request.body);
    const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
    if (!model) {
      return reply.code(404).send({ message: "模型不存在" });
    }
    if (!requireModelWrite(request, reply, model)) return;
    const name = assertValidFolderName(body.name);
    try {
      const brandId = findOrCreateDictionary("brands", body.brandName);
      const typeId = findOrCreateDictionary("model_types", body.typeName);
      const operationTree = body.operationTree === undefined ? model.operation_tree : normalizeOperationTree(body.operationTree);
      db.prepare(`
        UPDATE models
        SET name = ?, brand_id = ?, type_id = ?, operation_tree = ?, updated_at = CURRENT_TIMESTAMP
        WHERE id = ?
      `).run(name, brandId, typeId, operationTree, model.id);
      if (body.properties) {
        saveProperties(model.id, body.properties);
      }
      return { ok: true };
    } catch {
      return reply.code(409).send({ message: "当前目录下模型名称已存在" });
    }
  });

  app.put("/api/models/:id/thumbnail", { preHandler: [requireAuth] }, async (request, reply) => {
    const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
    const body = thumbnailSchema.parse(request.body);
    const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
    if (!model) {
      return reply.code(404).send({ message: "模型不存在" });
    }
    if (!requireModelWrite(request, reply, model)) return;
    const image = dataImageToBuffer(body.thumbnail);
    const stored = await storage.putObject(`thumbnails/model-${model.id}-${nanoid(8)}.${image.ext}`, image.buffer);
    try {
      db.prepare(`
        UPDATE models
        SET thumbnail_path = ?, thumbnail_provider = ?, updated_at = CURRENT_TIMESTAMP
        WHERE id = ?
      `).run(stored.key, stored.provider, model.id);
      if (model.thumbnail_path && model.thumbnail_provider) {
        await deleteStoredObject(model.thumbnail_path, model.thumbnail_provider);
      }
      return { ok: true, thumbnail_url: publicObjectUrl(stored.key, stored.provider) };
    } catch (error) {
      await storage.deleteObject(stored.key);
      throw error;
    }
  });

  app.delete("/api/models/:id", { preHandler: [requireAuth] }, async (request, reply) => {
    const params = z.object({ id: z.coerce.number().int() }).parse(request.params);
    const model = db.prepare("SELECT * FROM models WHERE id = ?").get(params.id) as ModelRow | undefined;
    if (!model) {
      return reply.code(404).send({ message: "模型不存在" });
    }
    if (!requireModelWrite(request, reply, model)) return;
    db.prepare("DELETE FROM models WHERE id = ?").run(model.id);
    await deleteStoredObject(model.file_path, model.storage_provider);
    if (model.thumbnail_path && model.thumbnail_provider) {
      await deleteStoredObject(model.thumbnail_path, model.thumbnail_provider);
    }
    return { ok: true };
  });
}
