-- ==========================================================
-- DMS v0.5.0
-- Database v5.1 - Procedures with Global Error Handlers
-- Server: Container Station on QNAPQNAP TS-473A
-- Database service: MariaDB 10.11
-- database ui: phpmyadmin 5-apache
-- backend sevice: node.js
-- frontend sevice: next.js
-- reverse proxy: nginx 1.27-alpine
-- cron service: n8n
-- scripts: alpine:3.20
-- Notes:
--        - ใช้ EXIT HANDLER FOR SQLEXCEPTION เพื่อ ROLLBACK + LOG ERROR
--        - ใช้ GET DIAGNOSTICS CONDITION 1 เพื่อดึงข้อความผิดพลาด
--        - หลัง LOG แล้ว RESIGNAL กลับให้แอปเห็น error ตามจริง
-- ==========================================================

/* ===========================================================
   
   ===========================================================*/
SET NAMES utf8mb4;
SET time_zone = '+07:00';

/* ---------- Helpers ---------- */
DROP PROCEDURE IF EXISTS sp_audit_log_add;
DELIMITER $$
CREATE PROCEDURE sp_audit_log_add(
  IN p_user_id   INT,
  IN p_action    VARCHAR(100),
  IN p_entity    VARCHAR(50),
  IN p_entity_id VARCHAR(50),
  IN p_details   JSON
)
BEGIN
  INSERT INTO audit_logs(user_id, action, entity_type, entity_id, details_json, created_at)
  VALUES (p_user_id, p_action, p_entity, p_entity_id, p_details, NOW());
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_assert_org_in_project;
DELIMITER $$
CREATE PROCEDURE sp_assert_org_in_project(
  IN p_project_id INT,
  IN p_org_id     INT
)
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM project_parties
    WHERE project_id = p_project_id AND org_id = p_org_id AND deleted_at IS NULL
  ) THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Organization is not in project_parties';
  END IF;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_next_doc_no;
DELIMITER $$
CREATE PROCEDURE sp_next_doc_no(
  IN  p_project_id INT,
  IN  p_type_code  VARCHAR(20),
  OUT o_next_no    VARCHAR(100)
)
BEGIN
  SET o_next_no = NULL; -- ใส่นโยบายเลขเอกสารของคุณที่นี่ หากต้องการ
END$$
DELIMITER ;

/* ===========================================================
   1) CORRESPONDENCES
   ===========================================================*/

DROP PROCEDURE IF EXISTS sp_correspondence_create_first;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_create_first(
  IN  p_user_id INT,
  IN  p_project_id INT,
  IN  p_type_id INT,
  IN  p_status_id INT,
  IN  p_cor_no VARCHAR(100),
  IN  p_title VARCHAR(255),
  IN  p_originator_id INT,
  IN  p_recipient_id INT,
  IN  p_is_internal TINYINT,
  IN  p_keywords VARCHAR(255),
  IN  p_pdf_path VARCHAR(500),
  OUT o_corr_id INT
)
BEGIN
  DECLARE v_exists INT DEFAULT 0;
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.create.first','correspondences','-',JSON_OBJECT('msg',v_err,'no',p_cor_no));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT COUNT(*) INTO v_exists
  FROM correspondences
  WHERE project_id=p_project_id AND correspondence_number=p_cor_no;
  IF v_exists>0 THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Duplicate correspondence_number in project';
  END IF;

  IF p_originator_id IS NOT NULL THEN CALL sp_assert_org_in_project(p_project_id,p_originator_id); END IF;
  IF p_recipient_id  IS NOT NULL THEN CALL sp_assert_org_in_project(p_project_id,p_recipient_id);  END IF;

  INSERT INTO correspondences(
    root_id, correspondence_number, correspondence_type_id, correspondence_status_id,
    is_internal_communication, revision, project_id, originator_id, recipient_id,
    title, keywords, created_at, created_by, issued_date, updated_by, pdf_path
  ) VALUES (
    NULL, p_cor_no, p_type_id, p_status_id,
    IFNULL(p_is_internal,0), NULL, p_project_id, p_originator_id, p_recipient_id,
    p_title, p_keywords, NOW(), p_user_id, NULL, NULL, p_pdf_path
  );

  SET o_corr_id = LAST_INSERT_ID();
  UPDATE correspondences SET root_id=corr_id WHERE corr_id=o_corr_id AND root_id IS NULL;

  CALL sp_audit_log_add(p_user_id,'cor.create.first','correspondences',o_corr_id,JSON_OBJECT('no',p_cor_no,'title',p_title));
  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_correspondence_add_revision;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_add_revision(
  IN  p_user_id INT,
  IN  p_root_corr_id INT,
  IN  p_status_id INT,
  IN  p_revision VARCHAR(50),
  IN  p_title VARCHAR(255),
  IN  p_pdf_path VARCHAR(500),
  OUT o_corr_id INT
)
BEGIN
  DECLARE v_project_id INT;
  DECLARE v_cor_no VARCHAR(100);
  DECLARE v_type_id INT;
  DECLARE v_is_internal TINYINT;
  DECLARE v_originator_id INT;
  DECLARE v_recipient_id INT;
  DECLARE v_keywords VARCHAR(255);
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.add.rev','correspondences',p_root_corr_id,JSON_OBJECT('msg',v_err,'rev',p_revision));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT project_id, correspondence_number, correspondence_type_id,
         is_internal_communication, originator_id, recipient_id, keywords
    INTO v_project_id, v_cor_no, v_type_id, v_is_internal, v_originator_id, v_recipient_id, v_keywords
  FROM correspondences WHERE corr_id=p_root_corr_id;

  IF v_project_id IS NULL THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Root correspondence not found';
  END IF;

  IF EXISTS(SELECT 1 FROM correspondences
            WHERE root_id=p_root_corr_id AND revision=p_revision AND project_id=v_project_id) THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Duplicate revision for this root/project';
  END IF;

  INSERT INTO correspondences(
    root_id, correspondence_number, correspondence_type_id, correspondence_status_id,
    is_internal_communication, revision, project_id, originator_id, recipient_id,
    title, keywords, created_at, created_by, issued_date, updated_by, pdf_path
  ) VALUES (
    p_root_corr_id, v_cor_no, v_type_id, p_status_id,
    v_is_internal, p_revision, v_project_id, v_originator_id, v_recipient_id,
    p_title, v_keywords, NOW(), p_user_id, NULL, NULL, p_pdf_path
  );

  SET o_corr_id = LAST_INSERT_ID();
  CALL sp_audit_log_add(p_user_id,'cor.add.revision','correspondences',o_corr_id,JSON_OBJECT('root',p_root_corr_id,'rev',p_revision));
  COMMIT;
END$$
DELIMITER ;

-- PATCH: sp_correspondence_update_status (เพิ่มส่วนตรวจ transition ก่อน UPDATE)
-- เพิ่ม “ตาราง/วิว Allowed Status Transition” เพื่อตรวจ business rule บน 
-- sp_correspondence_update_status (ยังทำแบบ soft rule ใน proc)
DROP PROCEDURE IF EXISTS sp_correspondence_update_status;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_update_status(
  IN p_user_id INT,
  IN p_corr_id INT,
  IN p_status_id INT,
  IN p_set_issue_datetime TINYINT -- 0/1
)
BEGIN
  DECLARE v_msg TEXT;

  DECLARE v_curr_status_id INT DEFAULT NULL;
  DECLARE v_curr_code VARCHAR(50);
  DECLARE v_new_code  VARCHAR(50);
  DECLARE v_curr_grp  VARCHAR(6);
  DECLARE v_new_grp   VARCHAR(6);

  /* โหลดสถานะปัจจุบัน */
  SELECT c.correspondence_status_id, s.status_code
    INTO v_curr_status_id, v_curr_code
  FROM correspondences c
  JOIN correspondence_status s ON s.status_id = c.correspondence_status_id
  WHERE c.corr_id = p_corr_id;

  IF v_curr_status_id IS NULL THEN
    SET v_msg := 'Correspondence not found';
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
  END IF;

  /* โหลดสถานะใหม่ */
  SELECT status_code
    INTO v_new_code
  FROM correspondence_status
  WHERE status_id = p_status_id;

  IF v_new_code IS NULL THEN
    SET v_msg := 'Target status_id not found';
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
  END IF;

  /* จัดกลุ่มรหัสสถานะสำหรับตรวจทรานซิชัน */
  SET v_curr_grp := CASE WHEN v_curr_code = 'DRAFT' THEN 'DRAFT' ELSE LEFT(v_curr_code,3) END;
  SET v_new_grp  := CASE WHEN v_new_code  = 'DRAFT' THEN 'DRAFT' ELSE LEFT(v_new_code,3)  END;

  /* กันแก้จากสถานะปิด/ยกเลิก */
  IF v_curr_grp IN ('CLB','CCB') THEN
    SET v_msg := 'Cannot change status from CLOSED/CANCELED';
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
  END IF;

  /* กรณีสถานะใหม่ = เดิม */
  IF p_status_id = v_curr_status_id THEN
    IF p_set_issue_datetime = 1 THEN
      UPDATE correspondences
         SET issued_date = NOW(),
             updated_by  = p_user_id
       WHERE corr_id = p_corr_id;

      CALL sp_audit_log_add(
        p_user_id,
        'cor.update.status.noop.stamp',
        'correspondences',
        p_corr_id,
        JSON_OBJECT('status_code', v_curr_code, 'set_issue', p_set_issue_datetime)
      );
      /* จบการทำงาน */
    ELSE
      SET v_msg := 'No change: same status and no issue stamp';
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
    END IF;

  ELSE
    /* ตรวจ allowed transitions */
    IF NOT (
         (v_curr_grp = 'DRAFT' AND v_new_grp = 'SUB')
      OR (v_curr_grp = 'SUB'   AND v_new_grp IN ('REP','CCB'))
      OR (v_curr_grp = 'REP'   AND v_new_grp IN ('RSB','CLB'))
      OR (v_curr_grp = 'RSB'   AND v_new_grp IN ('REP','CCB'))
    ) THEN
      SET v_msg := CONCAT('Invalid status transition: ', v_curr_code, ' -> ', v_new_code);
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
    END IF;

    /* อัปเดตสถานะ */
    UPDATE correspondences
       SET correspondence_status_id = p_status_id,
           issued_date = CASE WHEN p_set_issue_datetime = 1 THEN NOW() ELSE issued_date END,
           updated_by  = p_user_id
     WHERE corr_id = p_corr_id;

    IF ROW_COUNT() = 0 THEN
      SET v_msg := 'Correspondence not found or no change';
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
    END IF;

    /* บันทึก audit */
    CALL sp_audit_log_add(
      p_user_id,
      'cor.update.status',
      'correspondences',
      p_corr_id,
      JSON_OBJECT(
        'from_code', v_curr_code,
        'to_code',   v_new_code,
        'status_id', p_status_id,
        'set_issue', p_set_issue_datetime
      )
    );
  END IF;
END$$
DELIMITER ;


DROP PROCEDURE IF EXISTS sp_correspondence_soft_delete;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_soft_delete(
  IN p_user_id INT,
  IN p_corr_id INT
)
BEGIN
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.soft.delete','correspondences',p_corr_id,JSON_OBJECT('msg',v_err));
    RESIGNAL;
  END;

  START TRANSACTION;

  UPDATE correspondences
  SET deleted_at=NOW(), updated_by=p_user_id
  WHERE corr_id=p_corr_id AND deleted_at IS NULL;

  IF ROW_COUNT()=0 THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Correspondence not found or already deleted';
  END IF;

  CALL sp_audit_log_add(p_user_id,'cor.soft.delete','correspondences',p_corr_id,JSON_OBJECT());
  COMMIT;
END$$
DELIMITER ;

/* CC ORGS */
DROP PROCEDURE IF EXISTS sp_correspondence_add_cc_org;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_add_cc_org(
  IN p_user_id INT,
  IN p_corr_id INT,
  IN p_org_id  INT
)
BEGIN
  DECLARE v_project_id INT;
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.cc.add','correspondences',p_corr_id,JSON_OBJECT('msg',v_err,'org',p_org_id));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT project_id INTO v_project_id FROM correspondences WHERE corr_id=p_corr_id;
  IF v_project_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Correspondence not found'; END IF;

  CALL sp_assert_org_in_project(v_project_id,p_org_id);

  INSERT IGNORE INTO correspondence_cc_recipients(corr_id,org_id) VALUES(p_corr_id,p_org_id);

  CALL sp_audit_log_add(p_user_id,'cor.cc.add','correspondences',p_corr_id,JSON_OBJECT('org',p_org_id));
  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_correspondence_remove_cc_org;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_remove_cc_org(
  IN p_user_id INT,
  IN p_corr_id INT,
  IN p_org_id  INT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.cc.remove','correspondences',p_corr_id,JSON_OBJECT('msg',v_err,'org',p_org_id));
    RESIGNAL;
  END;
  START TRANSACTION;

  DELETE FROM correspondence_cc_recipients WHERE corr_id=p_corr_id AND org_id=p_org_id;
  IF ROW_COUNT()=0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='CC org not found on this correspondence'; END IF;

  CALL sp_audit_log_add(p_user_id,'cor.cc.remove','correspondences',p_corr_id,JSON_OBJECT('org',p_org_id));
  COMMIT;
END$$
DELIMITER ;

/* LINK/UNLINK */
DROP PROCEDURE IF EXISTS sp_correspondence_link;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_link(
  IN p_user_id INT,
  IN p_src_corr_id INT,
  IN p_tgt_corr_id INT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.link','correspondences',p_src_corr_id,JSON_OBJECT('msg',v_err,'to',p_tgt_corr_id));
    RESIGNAL;
  END;

  START TRANSACTION;

  IF p_src_corr_id = p_tgt_corr_id THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Cannot link to itself';
  END IF;

  INSERT IGNORE INTO correspondence_references(src_corr_id,tgt_corr_id) VALUES(p_src_corr_id,p_tgt_corr_id);
  CALL sp_audit_log_add(p_user_id,'cor.link','correspondences',p_src_corr_id,JSON_OBJECT('to',p_tgt_corr_id));
  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_correspondence_unlink;
DELIMITER $$
CREATE PROCEDURE sp_correspondence_unlink(
  IN p_user_id INT,
  IN p_src_corr_id INT,
  IN p_tgt_corr_id INT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cor.unlink','correspondences',p_src_corr_id,JSON_OBJECT('msg',v_err,'to',p_tgt_corr_id));
    RESIGNAL;
  END;

  START TRANSACTION;

  DELETE FROM correspondence_references WHERE src_corr_id=p_src_corr_id AND tgt_corr_id=p_tgt_corr_id;
  IF ROW_COUNT()=0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Link not found'; END IF;

  CALL sp_audit_log_add(p_user_id,'cor.unlink','correspondences',p_src_corr_id,JSON_OBJECT('to',p_tgt_corr_id));
  COMMIT;
END$$
DELIMITER ;

/* ===========================================================
   2) TECHNICAL DOCS
   ===========================================================*/

DROP PROCEDURE IF EXISTS sp_techdoc_create_first;
DELIMITER $$
CREATE PROCEDURE sp_techdoc_create_first(
  IN  p_user_id INT,
  IN  p_project_id INT,
  IN  p_doc_type_id INT,
  IN  p_status_code_id INT,
  IN  p_title VARCHAR(255),
  IN  p_revision VARCHAR(50),
  IN  p_pdf_path VARCHAR(500),
  OUT o_doc_id INT,
  OUT o_original_id INT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.tdoc.create.first','technicaldocs','-',JSON_OBJECT('msg',v_err,'title',p_title));
    RESIGNAL;
  END;

  START TRANSACTION;

  INSERT INTO technicaldocs(
    document_type_id, original_id, revision, title, project_id,
    status_code_id, approve_code_id, created_at, created_by, updated_by, deleted_at, pdf_path
  ) VALUES (
    p_doc_type_id, 0, p_revision, p_title, p_project_id,
    p_status_code_id, NULL, NOW(), p_user_id, NULL, NULL, p_pdf_path
  );

  SET o_doc_id = LAST_INSERT_ID();
  UPDATE technicaldocs SET original_id=o_doc_id WHERE id=o_doc_id;
  SET o_original_id = o_doc_id;

  CALL sp_audit_log_add(p_user_id,'tdoc.create.first','technicaldocs',o_doc_id,JSON_OBJECT('rev',p_revision,'title',p_title));
  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_techdoc_add_revision;
DELIMITER $$
CREATE PROCEDURE sp_techdoc_add_revision(
  IN  p_user_id INT,
  IN  p_original_id INT,
  IN  p_project_id INT,
  IN  p_status_code_id INT,
  IN  p_title VARCHAR(255),
  IN  p_revision VARCHAR(50),
  IN  p_pdf_path VARCHAR(500),
  OUT o_doc_id INT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.tdoc.add.rev','technicaldocs',p_original_id,JSON_OBJECT('msg',v_err,'rev',p_revision));
    RESIGNAL;
  END;

  START TRANSACTION;

  IF EXISTS (SELECT 1 FROM technicaldocs WHERE original_id=p_original_id AND project_id=p_project_id AND revision=p_revision) THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Duplicate technicaldoc revision';
  END IF;

  INSERT INTO technicaldocs(
    document_type_id, original_id, revision, title, project_id,
    status_code_id, approve_code_id, created_at, created_by, pdf_path
  )
  SELECT document_type_id, p_original_id, p_revision, p_title, t.project_id,
         p_status_code_id, NULL, NOW(), p_user_id, p_pdf_path
  FROM technicaldocs t WHERE t.id=p_original_id;

  SET o_doc_id = LAST_INSERT_ID();
  CALL sp_audit_log_add(p_user_id,'tdoc.add.revision','technicaldocs',o_doc_id,JSON_OBJECT('orig',p_original_id,'rev',p_revision));
  COMMIT;
END$$
DELIMITER ;

/* ===========================================================
   3) RFA & TRANSMITTAL
   ===========================================================*/

DROP PROCEDURE IF EXISTS sp_rfa_create_with_items;
DELIMITER $$
CREATE PROCEDURE sp_rfa_create_with_items(
  IN  p_user_id INT,
  IN  p_project_id INT,
  IN  p_cor_status_id INT,
  IN  p_cor_no VARCHAR(100),
  IN  p_title VARCHAR(255),
  IN  p_originator_id INT,
  IN  p_recipient_id INT,
  IN  p_keywords VARCHAR(255),
  IN  p_pdf_path VARCHAR(500),
  IN  p_item_ids_json JSON,
  OUT o_rfa_corr_id INT
)
BEGIN
  DECLARE v_type_id INT;
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.rfa.create','correspondences','-',JSON_OBJECT('msg',v_err,'no',p_cor_no));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT type_id INTO v_type_id FROM correspondence_types WHERE type_code='RFA' LIMIT 1;
  IF v_type_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='RFA type not configured'; END IF;

  CALL sp_correspondence_create_first(
    p_user_id,p_project_id,v_type_id,p_cor_status_id,p_cor_no,
    p_title,p_originator_id,p_recipient_id,0,p_keywords,p_pdf_path,o_rfa_corr_id
  );

  BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE n INT;
    DECLARE v_doc_id INT;
    SET n = JSON_LENGTH(p_item_ids_json);
    WHILE i<n DO
      SET v_doc_id = JSON_EXTRACT(p_item_ids_json, CONCAT('$[',i,']'));
      IF v_doc_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Invalid techdoc id in JSON'; END IF;
      INSERT INTO rfa_items(rfa_corr_id,technical_doc_id) VALUES(o_rfa_corr_id,v_doc_id);
      SET i = i + 1;
    END WHILE;
  END;

  CALL sp_audit_log_add(p_user_id,'rfa.create','correspondences',o_rfa_corr_id,JSON_OBJECT('no',p_cor_no,'items',p_item_ids_json));
  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_transmittal_create_with_items;
DELIMITER $$
CREATE PROCEDURE sp_transmittal_create_with_items(
  IN  p_user_id INT,
  IN  p_project_id INT,
  IN  p_cor_status_id INT,
  IN  p_cor_no VARCHAR(100),
  IN  p_title VARCHAR(255),
  IN  p_originator_id INT,
  IN  p_recipient_id INT,
  IN  p_keywords VARCHAR(255),
  IN  p_pdf_path VARCHAR(500),
  IN  p_item_corr_ids_json JSON,
  OUT o_trans_corr_id INT
)
BEGIN
  DECLARE v_type_id INT;
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.transmittal.create','correspondences','-',JSON_OBJECT('msg',v_err,'no',p_cor_no));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT type_id INTO v_type_id FROM correspondence_types WHERE type_code='TRANSMITTAL' LIMIT 1;
  IF v_type_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='TRANSMITTAL type not configured'; END IF;

  CALL sp_correspondence_create_first(
    p_user_id,p_project_id,v_type_id,p_cor_status_id,p_cor_no,
    p_title,p_originator_id,p_recipient_id,0,p_keywords,p_pdf_path,o_trans_corr_id
  );

  BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE n INT;
    DECLARE v_item_corr_id INT;
    SET n = JSON_LENGTH(p_item_corr_ids_json);
    WHILE i<n DO
      SET v_item_corr_id = JSON_EXTRACT(p_item_corr_ids_json, CONCAT('$[',i,']'));
      IF v_item_corr_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Invalid item corr_id in JSON'; END IF;
      INSERT INTO transmittal_items(transmittal_corr_id,item_corr_id) VALUES(o_trans_corr_id,v_item_corr_id);
      SET i = i + 1;
    END WHILE;
  END;

  CALL sp_audit_log_add(p_user_id,'transmittal.create','correspondences',o_trans_corr_id,JSON_OBJECT('no',p_cor_no,'items',p_item_corr_ids_json));
  COMMIT;
END$$
DELIMITER ;

/* ===========================================================
   4) CIRCULATION
   ===========================================================*/

DROP PROCEDURE IF EXISTS sp_circulation_upsert;
DELIMITER $$
CREATE PROCEDURE sp_circulation_upsert(
  IN  p_user_id INT,
  IN  p_org_id INT,
  IN  p_cir_no VARCHAR(100),
  IN  p_title VARCHAR(255),
  IN  p_status_id INT,
  IN  p_corr_id INT,
  IN  p_note LONGTEXT,
  OUT o_cir_id INT
)
BEGIN
  DECLARE v_id INT;
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cir.upsert','circulations','-',JSON_OBJECT('msg',v_err,'no',p_cir_no));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT cir_id INTO v_id FROM circulations WHERE org_id=p_org_id AND cir_no=p_cir_no;

  IF v_id IS NULL THEN
    INSERT INTO circulations(org_id,cir_no,title,status_id,corr_id,note,created_at,created_by)
    VALUES(p_org_id,p_cir_no,p_title,p_status_id,p_corr_id,p_note,NOW(),p_user_id);
    SET o_cir_id = LAST_INSERT_ID();
    CALL sp_audit_log_add(p_user_id,'cir.create','circulations',o_cir_id,JSON_OBJECT('org',p_org_id,'no',p_cir_no));
  ELSE
    UPDATE circulations
      SET title=p_title,status_id=p_status_id,corr_id=p_corr_id,note=p_note,updated_by=p_user_id
    WHERE cir_id=v_id;
    SET o_cir_id = v_id;
    CALL sp_audit_log_add(p_user_id,'cir.update','circulations',o_cir_id,JSON_OBJECT('status',p_status_id));
  END IF;

  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_circulation_user_set;
DELIMITER $$
CREATE PROCEDURE sp_circulation_user_set(
  IN p_user_id INT,
  IN p_cir_id INT,
  IN p_target_user_id INT,
  IN p_is_main TINYINT,
  IN p_is_action TINYINT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.cir.user.set','circulations',p_cir_id,JSON_OBJECT('msg',v_err,'user',p_target_user_id));
    RESIGNAL;
  END;

  START TRANSACTION;

  INSERT INTO cir_users(cir_id,user_id,is_main,is_action)
  VALUES(p_cir_id,p_target_user_id,IFNULL(p_is_main,0),IFNULL(p_is_action,0))
  ON DUPLICATE KEY UPDATE is_main=VALUES(is_main), is_action=VALUES(is_action);

  CALL sp_audit_log_add(p_user_id,'cir.user.set','circulations',p_cir_id,JSON_OBJECT('user',p_target_user_id,'main',p_is_main,'action',p_is_action));
  COMMIT;
END$$
DELIMITER ;

/* ===========================================================
   5) PROJECT PARTY / CONTRACT DWG / SEARCH
   ===========================================================*/

DROP PROCEDURE IF EXISTS sp_project_party_add;
DELIMITER $$
CREATE PROCEDURE sp_project_party_add(
  IN p_user_id INT,
  IN p_project_id INT,
  IN p_org_id INT,
  IN p_role VARCHAR(20)
)
BEGIN
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.project.party.add','projects',p_project_id,JSON_OBJECT('msg',v_err,'org',p_org_id,'role',p_role));
    RESIGNAL;
  END;

  START TRANSACTION;

  INSERT INTO project_parties(project_id,org_id,role,created_at,created_by)
  VALUES(p_project_id,p_org_id,UPPER(p_role),NOW(),p_user_id)
  ON DUPLICATE KEY UPDATE role=VALUES(role), updated_by=p_user_id;

  CALL sp_audit_log_add(p_user_id,'project.party.add','projects',p_project_id,JSON_OBJECT('org',p_org_id,'role',p_role));
  COMMIT;
END$$
DELIMITER ;

DROP PROCEDURE IF EXISTS sp_contract_dwg_upsert;
DELIMITER $$
CREATE PROCEDURE sp_contract_dwg_upsert(
  IN  p_user_id INT,
  IN  p_project_id INT,
  IN  p_sub_cat_id INT,
  IN  p_condwg_no VARCHAR(255),
  IN  p_title VARCHAR(255),
  IN  p_volume INT,
  IN  p_sub_no INT,
  OUT o_condwg_id INT
)
BEGIN
  DECLARE v_id INT;
  DECLARE v_err TEXT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.condwg.upsert','contract_dwg','-',JSON_OBJECT('msg',v_err,'no',p_condwg_no));
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT condwg_id INTO v_id FROM contract_dwg WHERE project_id=p_project_id AND condwg_no=p_condwg_no;

  IF v_id IS NULL THEN
    INSERT INTO contract_dwg(condwg_no,title,sub_cat_id,volume,sub_no,project_id)
    VALUES(p_condwg_no,p_title,p_sub_cat_id,p_volume,p_sub_no,p_project_id);
    SET o_condwg_id = LAST_INSERT_ID();
    CALL sp_audit_log_add(p_user_id,'condwg.create','contract_dwg',o_condwg_id,JSON_OBJECT('no',p_condwg_no,'title',p_title));
  ELSE
    UPDATE contract_dwg
    SET title=p_title,sub_cat_id=p_sub_cat_id,volume=p_volume,sub_no=p_sub_no
    WHERE condwg_id=v_id;
    SET o_condwg_id = v_id;
    CALL sp_audit_log_add(p_user_id,'condwg.update','contract_dwg',o_condwg_id,JSON_OBJECT('no',p_condwg_no,'title',p_title));
  END IF;

  COMMIT;
END$$
DELIMITER ;

-- read-only: ใช้ SELECT เพียว ไม่จำเป็นต้อง START TRANSACTION/Handler (เพิ่มก็ได้)
DROP PROCEDURE IF EXISTS sp_search_correspondence;
DELIMITER $$
CREATE PROCEDURE sp_search_correspondence(
  IN p_project_id INT,
  IN p_q VARCHAR(255),
  IN p_type_id INT,
  IN p_status_id INT,
  IN p_offset INT,
  IN p_limit INT
)
BEGIN
  SET p_offset = IFNULL(p_offset,0);
  SET p_limit  = IFNULL(p_limit,50);

  SELECT
    c.corr_id, c.root_id, c.project_id,
    c.correspondence_number, c.revision,
    c.correspondence_type_id, c.correspondence_status_id,
    c.title, c.keywords, c.issued_date, c.created_at,
    c.originator_id, c.recipient_id, c.pdf_path
  FROM correspondences c
  WHERE c.project_id = p_project_id
    AND (p_type_id   IS NULL OR c.correspondence_type_id   = p_type_id)
    AND (p_status_id IS NULL OR c.correspondence_status_id = p_status_id)
    AND (
      p_q IS NULL OR p_q='' OR
      c.correspondence_number LIKE CONCAT('%',p_q,'%') OR
      c.title LIKE CONCAT('%',p_q,'%') OR
      c.keywords LIKE CONCAT('%',p_q,'%')
    )
  ORDER BY c.created_at DESC, c.corr_id DESC
  LIMIT p_offset, p_limit;
END$$
DELIMITER ;

/* ===========================================================
   STORED PROCEDURES BUNDLE (MariaDB 10.x)
   For normalized CONTRACT-DWG schema (project-scoped)
   =========================================================== */

DELIMITER $$

/* -----------------------------------------------------------
   Safety re-create: DROP IF EXISTS
   ----------------------------------------------------------- */
DROP PROCEDURE IF EXISTS sp_upsert_volume $$
DROP PROCEDURE IF EXISTS sp_upsert_cat $$
DROP PROCEDURE IF EXISTS sp_upsert_sub_cat $$
DROP PROCEDURE IF EXISTS sp_map_subcat_cat $$
DROP PROCEDURE IF EXISTS sp_map_subcat_cat_by_names $$
DROP PROCEDURE IF EXISTS sp_create_contract_dwg $$
DROP PROCEDURE IF EXISTS sp_update_contract_dwg $$
DROP PROCEDURE IF EXISTS sp_delete_contract_dwg $$
DROP PROCEDURE IF EXISTS sp_search_contract_dwg $$
DROP PROCEDURE IF EXISTS sp_bulk_stage_create $$
DROP PROCEDURE IF EXISTS sp_bulk_import_contract_dwg $$

/* ===========================================================
   sp_upsert_volume
   - ถ้ามี (project_id, volume_code) แล้ว → update name/desc
   - ถ้าไม่มี → insert
   - คืนค่า volume_id (OUT)
   =========================================================== */
CREATE PROCEDURE sp_upsert_volume (
    IN  p_project_id  INT,
    IN  p_volume_code VARCHAR(100),
    IN  p_volume_name VARCHAR(255),
    IN  p_description TEXT,
    OUT o_volume_id   INT
)
BEGIN
  DECLARE v_existing INT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK;
    RESIGNAL;
  END;

  START TRANSACTION;

  SELECT volume_id INTO v_existing
  FROM contract_dwg_volume
  WHERE project_id = p_project_id
    AND volume_code = TRIM(p_volume_code)
  LIMIT 1;

  IF v_existing IS NULL THEN
    INSERT INTO contract_dwg_volume (project_id, volume_code, volume_name, description)
    VALUES (p_project_id, TRIM(p_volume_code), NULLIF(TRIM(p_volume_name),''), p_description);
    SET o_volume_id = LAST_INSERT_ID();
  ELSE
    UPDATE contract_dwg_volume
       SET volume_name = NULLIF(TRIM(p_volume_name),''),
           description = p_description
     WHERE volume_id = v_existing
       AND project_id = p_project_id;
    SET o_volume_id = v_existing;
  END IF;

  COMMIT;
END $$

/* ===========================================================
   sp_upsert_cat
   - upsert (project_id, cat_name) ; อัปเดต cat_code ได้
   - คืน cat_id (OUT)
   =========================================================== */
CREATE PROCEDURE sp_upsert_cat (
    IN  p_project_id INT,
    IN  p_cat_name   VARCHAR(255),
    IN  p_cat_code   VARCHAR(100),
    OUT o_cat_id     INT
)
BEGIN
  DECLARE v_existing INT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  SELECT cat_id INTO v_existing
  FROM contract_dwg_cat
  WHERE project_id = p_project_id
    AND cat_name = TRIM(p_cat_name)
  LIMIT 1;

  IF v_existing IS NULL THEN
    INSERT INTO contract_dwg_cat (project_id, cat_name, cat_code)
    VALUES (p_project_id, TRIM(p_cat_name), NULLIF(TRIM(p_cat_code),''));
    SET o_cat_id = LAST_INSERT_ID();
  ELSE
    UPDATE contract_dwg_cat
       SET cat_code = NULLIF(TRIM(p_cat_code),'')
     WHERE cat_id = v_existing AND project_id = p_project_id;
    SET o_cat_id = v_existing;
  END IF;

  COMMIT;
END $$

/* ===========================================================
   sp_upsert_sub_cat
   - upsert (project_id, sub_cat_name) ; อัปเดต sub_cat_code ได้
   - คืน sub_cat_id (OUT)
   =========================================================== */
CREATE PROCEDURE sp_upsert_sub_cat (
    IN  p_project_id   INT,
    IN  p_sub_cat_name VARCHAR(255),
    IN  p_sub_cat_code VARCHAR(100),
    OUT o_sub_cat_id   INT
)
BEGIN
  DECLARE v_existing INT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  SELECT sub_cat_id INTO v_existing
  FROM contract_dwg_sub_cat
  WHERE project_id = p_project_id
    AND sub_cat_name = TRIM(p_sub_cat_name)
  LIMIT 1;

  IF v_existing IS NULL THEN
    INSERT INTO contract_dwg_sub_cat (project_id, sub_cat_name, sub_cat_code)
    VALUES (p_project_id, TRIM(p_sub_cat_name), NULLIF(TRIM(p_sub_cat_code),''));
    SET o_sub_cat_id = LAST_INSERT_ID();
  ELSE
    UPDATE contract_dwg_sub_cat
       SET sub_cat_code = NULLIF(TRIM(p_sub_cat_code),'')
     WHERE sub_cat_id = v_existing AND project_id = p_project_id;
    SET o_sub_cat_id = v_existing;
  END IF;

  COMMIT;
END $$

/* ===========================================================
   sp_map_subcat_cat
   - ผูก sub_cat_id ↔ cat_id ภายในโปรเจกต์ (id-based)
   - idempotent: map ซ้ำไม่ error
   =========================================================== */
CREATE PROCEDURE sp_map_subcat_cat (
    IN p_project_id INT,
    IN p_sub_cat_id INT,
    IN p_cat_id     INT
)
BEGIN
  DECLARE v_cnt INT DEFAULT 0;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  -- ตรวจว่า sub_cat อยู่โปรเจกต์เดียวกัน
  SELECT COUNT(*) INTO v_cnt
  FROM contract_dwg_sub_cat
  WHERE project_id = p_project_id
    AND sub_cat_id = p_sub_cat_id;
  IF v_cnt = 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Invalid sub_cat_id for this project';
  END IF;

  -- ตรวจว่า cat อยู่โปรเจกต์เดียวกัน
  SELECT COUNT(*) INTO v_cnt
  FROM contract_dwg_cat
  WHERE project_id = p_project_id
    AND cat_id = p_cat_id;
  IF v_cnt = 0 THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Invalid cat_id for this project';
  END IF;

  -- ทำ mapping ถ้ายังไม่มี
  INSERT IGNORE INTO contract_dwg_subcat_cat_map (project_id, sub_cat_id, cat_id)
  VALUES (p_project_id, p_sub_cat_id, p_cat_id);

  COMMIT;
END $$

/* ===========================================================
   sp_map_subcat_cat_by_names
   - ผูกโดยระบุชื่อ: sub_cat_name, cat_name (ช่วยงาน UI/import)
   - ถ้าไม่มี sub_cat/cat → จะ error (เพื่อความชัดเจน)
   =========================================================== */
CREATE PROCEDURE sp_map_subcat_cat_by_names (
    IN p_project_id   INT,
    IN p_sub_cat_name VARCHAR(255),
    IN p_cat_name     VARCHAR(255)
)
BEGIN
  DECLARE v_sub_cat_id INT;
  DECLARE v_cat_id     INT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  SELECT sub_cat_id INTO v_sub_cat_id
  FROM contract_dwg_sub_cat
  WHERE project_id = p_project_id
    AND sub_cat_name = TRIM(p_sub_cat_name)
  LIMIT 1;

  IF v_sub_cat_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'sub_cat_name not found in this project';
  END IF;

  SELECT cat_id INTO v_cat_id
  FROM contract_dwg_cat
  WHERE project_id = p_project_id
    AND cat_name = TRIM(p_cat_name)
  LIMIT 1;

  IF v_cat_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'cat_name not found in this project';
  END IF;

  INSERT IGNORE INTO contract_dwg_subcat_cat_map (project_id, sub_cat_id, cat_id)
  VALUES (p_project_id, v_sub_cat_id, v_cat_id);

  COMMIT;
END $$

/* ===========================================================
   sp_create_contract_dwg
   - สร้าง drawing ใหม่โดยระบุ:
       project_id, condwg_no, title,
       volume_code (อ้าง volume), sub_cat_name (อ้าง subcat),
       sub_no, remark
   - ถ้าไม่มี volume/subcat → error (ชัดเจน)
   - คืน condwg_id (OUT)
   =========================================================== */
CREATE PROCEDURE sp_create_contract_dwg (
    IN  p_project_id  INT,
    IN  p_condwg_no   VARCHAR(255),
    IN  p_title       VARCHAR(255),
    IN  p_volume_code VARCHAR(100),
    IN  p_sub_cat_name VARCHAR(255),
    IN  p_sub_no      INT,
    IN  p_remark      VARCHAR(500),
    OUT o_condwg_id   INT
)
BEGIN
  DECLARE v_volume_id  INT;
  DECLARE v_sub_cat_id INT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  -- หา volume_id
  SELECT volume_id INTO v_volume_id
  FROM contract_dwg_volume
  WHERE project_id = p_project_id
    AND volume_code = TRIM(p_volume_code)
  LIMIT 1;

  IF v_volume_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'volume_code not found in this project';
  END IF;

  -- หา sub_cat_id
  SELECT sub_cat_id INTO v_sub_cat_id
  FROM contract_dwg_sub_cat
  WHERE project_id = p_project_id
    AND sub_cat_name = TRIM(p_sub_cat_name)
  LIMIT 1;

  IF v_sub_cat_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'sub_cat_name not found in this project';
  END IF;

  -- Insert drawing
  INSERT INTO contract_dwg (
    project_id, condwg_no, title, sub_cat_id, volume_id, sub_no, remark
  ) VALUES (
    p_project_id, TRIM(p_condwg_no), NULLIF(TRIM(p_title), ''), v_sub_cat_id, v_volume_id, p_sub_no, NULLIF(TRIM(p_remark),'')
  );

  SET o_condwg_id = LAST_INSERT_ID();

  COMMIT;
END $$

/* ===========================================================
   sp_update_contract_dwg
   - อัปเดต drawing ตาม condwg_id
   - สามารถเปลี่ยน volume_code / sub_cat_name ได้ (ใน project เดิม)
   =========================================================== */
CREATE PROCEDURE sp_update_contract_dwg (
    IN p_condwg_id    INT,
    IN p_title        VARCHAR(255),
    IN p_volume_code  VARCHAR(100),
    IN p_sub_cat_name VARCHAR(255),
    IN p_sub_no       INT,
    IN p_remark       VARCHAR(500)
)
BEGIN
  DECLARE v_project_id INT;
  DECLARE v_volume_id  INT;
  DECLARE v_sub_cat_id INT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  -- ดึง project_id เดิมของ drawing
  SELECT project_id INTO v_project_id
  FROM contract_dwg
  WHERE condwg_id = p_condwg_id
  LIMIT 1;

  IF v_project_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'condwg_id not found';
  END IF;

  -- resolve volume_id ตาม project เดิม
  SELECT volume_id INTO v_volume_id
  FROM contract_dwg_volume
  WHERE project_id = v_project_id
    AND volume_code = TRIM(p_volume_code)
  LIMIT 1;

  IF v_volume_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'volume_code not found in this project';
  END IF;

  -- resolve sub_cat_id ตาม project เดิม
  SELECT sub_cat_id INTO v_sub_cat_id
  FROM contract_dwg_sub_cat
  WHERE project_id = v_project_id
    AND sub_cat_name = TRIM(p_sub_cat_name)
  LIMIT 1;

  IF v_sub_cat_id IS NULL THEN
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'sub_cat_name not found in this project';
  END IF;

  UPDATE contract_dwg
     SET title     = NULLIF(TRIM(p_title),''),
         volume_id = v_volume_id,
         sub_cat_id= v_sub_cat_id,
         sub_no    = p_sub_no,
         remark    = NULLIF(TRIM(p_remark),'')
   WHERE condwg_id = p_condwg_id;

  COMMIT;
END $$

/* ===========================================================
   sp_delete_contract_dwg
   - ลบ drawing ตาม condwg_id
   - ถ้าต้องการ soft-delete: เปลี่ยนเป็น set deleted_at ได้
   =========================================================== */
CREATE PROCEDURE sp_delete_contract_dwg (
    IN p_condwg_id INT
)
BEGIN
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  DELETE FROM contract_dwg
   WHERE condwg_id = p_condwg_id;

  COMMIT;
END $$

/* ===========================================================
   sp_search_contract_dwg
   - ค้นหาในโปรเจกต์ด้วย keyword ง่าย ๆ (condwg_no/title/remark)
   - คืนผลลัพธ์พร้อม volume / sub-cat และ category list
   =========================================================== */
CREATE PROCEDURE sp_search_contract_dwg (
    IN p_project_id INT,
    IN p_keyword    VARCHAR(255)  -- ใส่ NULL/'' เพื่อดึงทั้งหมดของโปรเจกต์
)
BEGIN
  SET p_keyword = IFNULL(TRIM(p_keyword), '');
  IF p_keyword = '' THEN
    -- all in project
    SELECT
      d.condwg_id, d.project_id, d.condwg_no, d.title, d.sub_no, d.remark,
      v.volume_code, v.volume_name,
      sc.sub_cat_name,
      (SELECT GROUP_CONCAT(DISTINCT c.cat_name ORDER BY c.cat_name SEPARATOR ', ')
         FROM contract_dwg_subcat_cat_map m
         JOIN contract_dwg_cat c
           ON c.project_id = m.project_id AND c.cat_id = m.cat_id
        WHERE m.project_id = d.project_id
          AND m.sub_cat_id = d.sub_cat_id
      ) AS categories
    FROM contract_dwg d
    JOIN contract_dwg_volume v
      ON v.project_id = d.project_id AND v.volume_id = d.volume_id
    JOIN contract_dwg_sub_cat sc
      ON sc.project_id = d.project_id AND sc.sub_cat_id = d.sub_cat_id
    WHERE d.project_id = p_project_id
    ORDER BY d.condwg_no;
  ELSE
    -- filtered
    SELECT
      d.condwg_id, d.project_id, d.condwg_no, d.title, d.sub_no, d.remark,
      v.volume_code, v.volume_name,
      sc.sub_cat_name,
      (SELECT GROUP_CONCAT(DISTINCT c.cat_name ORDER BY c.cat_name SEPARATOR ', ')
         FROM contract_dwg_subcat_cat_map m
         JOIN contract_dwg_cat c
           ON c.project_id = m.project_id AND c.cat_id = m.cat_id
        WHERE m.project_id = d.project_id
          AND m.sub_cat_id = d.sub_cat_id
      ) AS categories
    FROM contract_dwg d
    JOIN contract_dwg_volume v
      ON v.project_id = d.project_id AND v.volume_id = d.volume_id
    JOIN contract_dwg_sub_cat sc
      ON sc.project_id = d.project_id AND sc.sub_cat_id = d.sub_cat_id
    WHERE d.project_id = p_project_id
      AND (
        d.condwg_no LIKE CONCAT('%', p_keyword, '%')
        OR IFNULL(d.title,'')  LIKE CONCAT('%', p_keyword, '%')
        OR IFNULL(d.remark,'') LIKE CONCAT('%', p_keyword, '%')
        OR v.volume_code LIKE CONCAT('%', p_keyword, '%')
        OR IFNULL(v.volume_name,'') LIKE CONCAT('%', p_keyword, '%')
        OR sc.sub_cat_name LIKE CONCAT('%', p_keyword, '%')
      )
    ORDER BY d.condwg_no;
  END IF;
END $$

/* ===========================================================
   (ตัวเลือก) Bulk Import ผ่านตาราง stage
   - ช่วยนำเข้า drawings เป็นชุดจาก UI/CSV/XLSX (แปลงเข้า stage ก่อน)
   - รองรับ UPsert ของ volume/subcat (ถ้าต้องการ strict ใช้ flag)
   =========================================================== */

-- 1) สร้าง/รีเซ็ตตาราง stage ด้วย procedure
CREATE PROCEDURE sp_bulk_stage_create ()
BEGIN
  DROP TABLE IF EXISTS contract_dwg_stage;
  CREATE TABLE contract_dwg_stage (
    stage_id     INT AUTO_INCREMENT PRIMARY KEY,
    project_id   INT NOT NULL,
    condwg_no    VARCHAR(255) NOT NULL,
    title        VARCHAR(255) NULL,
    volume_code  VARCHAR(100) NOT NULL,
    sub_cat_name VARCHAR(255) NOT NULL,
    sub_no       INT NULL,
    remark       VARCHAR(500) NULL
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
END $$

-- 2) import จาก stage → ตารางจริง
--   - p_upsert_refs = 1 → ถ้าไม่พบ volume/subcat จะ upsert ให้
--   - p_upsert_refs = 0 → ถ้าไม่พบ จะ error และยกเลิกทั้ง batch
DELIMITER $$

DROP PROCEDURE IF EXISTS sp_bulk_import_contract_dwg $$
CREATE PROCEDURE sp_bulk_import_contract_dwg (
    IN p_upsert_refs TINYINT -- 0/1
)
BEGIN
  DECLARE v_done INT DEFAULT 0;
  DECLARE v_project_id INT;
  DECLARE v_condwg_no VARCHAR(255);
  DECLARE v_title VARCHAR(255);
  DECLARE v_volume_code VARCHAR(100);
  DECLARE v_sub_cat_name VARCHAR(255);
  DECLARE v_sub_no INT;
  DECLARE v_remark VARCHAR(500);

  DECLARE v_volume_id INT;
  DECLARE v_sub_cat_id INT;

  -- ใช้สำหรับข้อความ error ใน SIGNAL (MESSAGE_TEXT ต้องเป็นตัวแปร/ลิตเตอรัล)
  DECLARE v_msg VARCHAR(512);

  DECLARE cur CURSOR FOR
    SELECT project_id, condwg_no, title, volume_code, sub_cat_name, sub_no, remark
    FROM contract_dwg_stage
    ORDER BY stage_id;

  DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK; RESIGNAL;
  END;

  START TRANSACTION;

  OPEN cur;
  read_loop: LOOP
    FETCH cur INTO v_project_id, v_condwg_no, v_title, v_volume_code, v_sub_cat_name, v_sub_no, v_remark;
    IF v_done = 1 THEN
      LEAVE read_loop;
    END IF;

    /* ---------- resolve volume ---------- */
    SELECT volume_id INTO v_volume_id
    FROM contract_dwg_volume
    WHERE project_id = v_project_id
      AND volume_code = TRIM(v_volume_code)
    LIMIT 1;

    IF v_volume_id IS NULL THEN
      IF p_upsert_refs = 1 THEN
        INSERT INTO contract_dwg_volume (project_id, volume_code, volume_name)
        VALUES (v_project_id, TRIM(v_volume_code), NULL);
        SET v_volume_id = LAST_INSERT_ID();
      ELSE
        SET v_msg = CONCAT('volume_code not found: ', TRIM(IFNULL(v_volume_code,'')));
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
      END IF;
    END IF;

    /* ---------- resolve sub_cat ---------- */
    SELECT sub_cat_id INTO v_sub_cat_id
    FROM contract_dwg_sub_cat
    WHERE project_id = v_project_id
      AND sub_cat_name = TRIM(v_sub_cat_name)
    LIMIT 1;

    IF v_sub_cat_id IS NULL THEN
      IF p_upsert_refs = 1 THEN
        INSERT INTO contract_dwg_sub_cat (project_id, sub_cat_name)
        VALUES (v_project_id, TRIM(v_sub_cat_name));
        SET v_sub_cat_id = LAST_INSERT_ID();
      ELSE
        SET v_msg = CONCAT('sub_cat_name not found: ', TRIM(IFNULL(v_sub_cat_name,'')));
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msg;
      END IF;
    END IF;

    /* ---------- upsert drawing (unique: project_id+condwg_no) ---------- */
    INSERT INTO contract_dwg (
      project_id, condwg_no, title, sub_cat_id, volume_id, sub_no, remark
    ) VALUES (
      v_project_id, TRIM(v_condwg_no), NULLIF(TRIM(v_title),''), v_sub_cat_id, v_volume_id, v_sub_no, NULLIF(TRIM(v_remark),'')
    )
    ON DUPLICATE KEY UPDATE
      title      = VALUES(title),
      sub_cat_id = VALUES(sub_cat_id),
      volume_id  = VALUES(volume_id),
      sub_no     = VALUES(sub_no),
      remark     = VALUES(remark);

  END LOOP;
  CLOSE cur;

  COMMIT;
END $$

DELIMITER ;

-- 3) โปรซีเยอร์ลิงก์ “DWG 1 แผ่น → หลาย Contract Drawing”
-- มี Transaction + Global Error Handler (ใช้ sp_audit_log_add ที่เรามีอยู่แล้ว)
DROP PROCEDURE IF EXISTS sp_dwgdoc_link_contract_dwgs;
DELIMITER $$
CREATE PROCEDURE sp_dwgdoc_link_contract_dwgs(
  IN  p_user_id INT,
  IN  p_technical_doc_id INT,      -- ต้องเป็น DWG
  IN  p_condwg_ids_json JSON       -- ตัวอย่าง: [201,202,205]
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE i INT DEFAULT 0;
  DECLARE n INT;
  DECLARE v_condwg_id INT;

  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.dwgdoc.link.dwgs','technicaldocs',
                          p_technical_doc_id, JSON_OBJECT('msg',v_err));
    RESIGNAL;
  END;

  START TRANSACTION;

  /* ยืนยันว่า dwg doc มีอยู่จริงและเป็น DWG (ทริกเกอร์ก็เช็คอีกชั้นตอน insert) */
  IF NOT EXISTS (
    SELECT 1
    FROM technicaldocs t
    JOIN document_types dt ON dt.document_types_id = t.document_type_id
    WHERE t.id = p_technical_doc_id AND dt.code = 'DWG'
  ) THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='technical_doc not found or not DWG';
  END IF;

  SET n = JSON_LENGTH(p_condwg_ids_json);
  WHILE i < n DO
    SET v_condwg_id = JSON_EXTRACT(p_condwg_ids_json, CONCAT('$[',i,']'));
    IF v_condwg_id IS NULL THEN
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Invalid condwg_id in JSON';
    END IF;

    INSERT INTO dwgdoc_contract_dwg(technical_doc_id, condwg_id)
    VALUES (p_technical_doc_id, v_condwg_id);

    SET i = i + 1;
  END WHILE;

  CALL sp_audit_log_add(p_user_id,'dwgdoc.link.dwgs','technicaldocs',
                        p_technical_doc_id, JSON_OBJECT('condwg_ids',p_condwg_ids_json));
  COMMIT;
END$$
DELIMITER ;

-- องการ “ลบความเชื่อมโยงทั้งหมดของ DWG หนึ่งแผ่น” ก็เพิ่ม proc เสริม:
DROP PROCEDURE IF EXISTS sp_dwgdoc_unlink_all_dwgs;
DELIMITER $$
CREATE PROCEDURE sp_dwgdoc_unlink_all_dwgs(
  IN p_user_id INT,
  IN p_technical_doc_id INT
)
BEGIN
  DECLARE v_err TEXT;
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    GET DIAGNOSTICS CONDITION 1 v_err = MESSAGE_TEXT;
    ROLLBACK;
    CALL sp_audit_log_add(p_user_id,'_err.dwgdoc.unlink.all','technicaldocs',
                          p_technical_doc_id, JSON_OBJECT('msg',v_err));
    RESIGNAL;
  END;

  START TRANSACTION;
  DELETE FROM dwgdoc_contract_dwg WHERE technical_doc_id = p_technical_doc_id;
  CALL sp_audit_log_add(p_user_id,'dwgdoc.unlink.all','technicaldocs',
                        p_technical_doc_id, JSON_OBJECT());
  COMMIT;
END$$
DELIMITER ;

-- ---------------------------------------------------------------------
-- Add Procedure from ChatGPT ปรับปรุง V5.1 -> V5.2
-- ---------------------------------------------------------------------

/* A2) Procedures ที่ควรมี
1) ตั้ง/ปลดล็อคผู้ใช้ (failed login/locked_until) + mark last_login 
   สอดคล้องคอลัมน์ใน users (failed_attempts/locked_until/last_login_at) ที่มีอยู่แล้ว*/
-- NEW: sp_user_login_success_mark
DROP PROCEDURE IF EXISTS sp_user_login_success_mark;
DELIMITER $$
CREATE PROCEDURE sp_user_login_success_mark(IN p_user_id INT)
BEGIN
  UPDATE users
  SET failed_attempts = 0,
      locked_until = NULL,
      last_login_at = NOW()
  WHERE user_id = p_user_id;
  CALL sp_audit_log_add(p_user_id,'user.login.success','users',p_user_id, JSON_OBJECT());
END$$
DELIMITER ;

-- NEW: sp_user_login_fail_mark
DROP PROCEDURE IF EXISTS sp_user_login_fail_mark;
DELIMITER $$
CREATE PROCEDURE sp_user_login_fail_mark(IN p_user_id INT, IN p_threshold INT, IN p_lock_minutes INT)
BEGIN
  UPDATE users
  SET failed_attempts = failed_attempts + 1,
      locked_until = CASE WHEN failed_attempts + 1 >= p_threshold
                          THEN DATE_ADD(NOW(), INTERVAL p_lock_minutes MINUTE)
                          ELSE locked_until END
  WHERE user_id = p_user_id;
  CALL sp_audit_log_add(p_user_id,'user.login.fail','users',p_user_id, JSON_OBJECT('threshold',p_threshold));
END$$
DELIMITER ;

-- 2) จัดการบทบาทแบบ atomic + log อ้างอิงโครง roles/user_roles/user_project_roles ที่มีในสคีมาเดิม
-- NEW: sp_user_role_set (global)
DROP PROCEDURE IF EXISTS sp_user_role_set;
DELIMITER $$
CREATE PROCEDURE sp_user_role_set(IN p_actor_user_id INT, IN p_target_user_id INT, IN p_role_code VARCHAR(50), IN p_enable TINYINT)
BEGIN
  DECLARE v_role_id INT;
  SELECT role_id INTO v_role_id FROM roles WHERE role_code=p_role_code;
  IF v_role_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='role_code not found'; END IF;

  IF p_enable=1 THEN
    INSERT IGNORE INTO user_roles(user_id,role_id) VALUES(p_target_user_id,v_role_id);
  ELSE
    DELETE FROM user_roles WHERE user_id=p_target_user_id AND role_id=v_role_id;
  END IF;

  CALL sp_audit_log_add(p_actor_user_id,'user.role.set','users',p_target_user_id,
                        JSON_OBJECT('role',p_role_code,'enable',p_enable));
END$$
DELIMITER ;

-- NEW: sp_user_project_role_set (scoped)
DROP PROCEDURE IF EXISTS sp_user_project_role_set;
DELIMITER $$
CREATE PROCEDURE sp_user_project_role_set(IN p_actor_user_id INT, IN p_target_user_id INT, IN p_project_id INT, IN p_role_code VARCHAR(50), IN p_enable TINYINT)
BEGIN
  DECLARE v_role_id INT;
  SELECT role_id INTO v_role_id FROM roles WHERE role_code=p_role_code;
  IF v_role_id IS NULL THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='role_code not found'; END IF;

  IF p_enable=1 THEN
    INSERT IGNORE INTO user_project_roles(user_id,project_id,role_id)
    VALUES(p_target_user_id,p_project_id,v_role_id);
  ELSE
    DELETE FROM user_project_roles WHERE user_id=p_target_user_id AND project_id=p_project_id AND role_id=v_role_id;
  END IF;

  CALL sp_audit_log_add(p_actor_user_id,'user.project_role.set','users',p_target_user_id,
                        JSON_OBJECT('project_id',p_project_id,'role',p_role_code,'enable',p_enable));
END$$
DELIMITER ;
/*
 B) ส่วน Correspondences (procedures, views)
B1) ปรับปรุง/เติมเต็มที่ควรมี
ทำ sp_next_doc_no ให้ใช้งานจริง (เลขเอกสารรายโปรเจกต์ + รายชนิด) 
จะช่วยให้ UI เรียกเลขเอกสารแบบมาตรฐาน (แทน placeholder เดิม)*/
-- REPLACE: sp_next_doc_no (ใช้งานจริง)
DROP PROCEDURE IF EXISTS sp_next_doc_no;
DELIMITER $$
CREATE PROCEDURE sp_next_doc_no(
  IN  p_project_id INT,
  IN  p_type_code  VARCHAR(20),
  OUT o_next_no    VARCHAR(100)
)
BEGIN
  DECLARE v_next INT;
  START TRANSACTION;
    INSERT INTO doc_no_sequences(project_id,type_code,last_no)
      VALUES(p_project_id,p_type_code,0)
      ON DUPLICATE KEY UPDATE last_no=last_no;
    UPDATE doc_no_sequences
      SET last_no = last_no + 1
      WHERE project_id=p_project_id AND type_code=p_type_code;
    SELECT last_no INTO v_next
      FROM doc_no_sequences
      WHERE project_id=p_project_id AND type_code=p_type_code;
  COMMIT;

  -- ตัวอย่างรูปแบบ: <TYPE>-<PROJECT>-<RUN> → RFA-LCBP3C1-000123
  -- SET o_next_no = CONCAT(p_type_code,'-',(SELECT project_code FROM projects WHERE project_id=p_project_id),'-',LPAD(v_next,6,'0'));
END$$
DELIMITER ;

/* C2) ข้อเสนอเติมเต็ม
Procedure: เพิ่ม “ตอบ/อัปเดตสถานะ RFA” ที่หุ้ม sp_correspondence_update_status และ auto set issued_date ตาม flag 
(คุณมี proc update status แล้ว เพียงสร้าง wrapper ให้ UI เรียกตรง ๆ)*/

-- NEW: sp_rfa_update_status
DROP PROCEDURE IF EXISTS sp_rfa_update_status;
DELIMITER $$
CREATE PROCEDURE sp_rfa_update_status(IN p_user_id INT, IN p_rfa_corr_id INT, IN p_status_id INT, IN p_set_issue TINYINT)
BEGIN
  CALL sp_correspondence_update_status(p_user_id, p_rfa_corr_id, p_status_id, p_set_issue);
  CALL sp_audit_log_add(p_user_id,'rfa.update.status','correspondences',p_rfa_corr_id,
                        JSON_OBJECT('status_id',p_status_id,'set_issue',p_set_issue));
END$$
DELIMITER ;
