<?php

namespace lib\Core\Database;

use PDO;
use PDOStatement;

class QueryBuilder
{
  private bool $_debug = false;
  protected PDO $pdo;
  protected string $latestQuery = "";
  protected string $table = '';
  protected array $select = ['*'];
  protected array $where = [];
  protected array $bindings = [];
  protected array $order = [];
  protected array $joins = [];
  protected ?int $limit = null;
  protected ?int $offset = null;

  protected function __construct(PDO $pdo)
  {
    $this->pdo = $pdo;
  }
  final public function setDebug(bool $debug): void
  {
    $this->_debug = $debug;
  }
  final public function getLastQuery(): string
  {
    return $this->latestQuery;
  }
  final public function table(string $table): static
  {
    $this->table = $table;
    return $this;
  }

  protected function buildSelectSql(?string $select = null): string
  {
    $select = $select ?? implode(', ', $this->select);
    $sql = "SELECT {$select} FROM {$this->table}";
    // ⬇️ Join 처리 추가
    if (!empty($this->joins)) {
      $sql .= ' ' . implode(' ', $this->joins);
    }
    // Where 처리
    if (!empty($this->where)) {
      $sql .= " WHERE " . $this->buildWhereSql();
    }
    // Order 처리
    if (!empty($this->order)) {
      $sql .= " ORDER BY " . implode(', ', $this->order);
    }
    if ($this->limit) {
      $sql .= " LIMIT {$this->limit}";
    }
    if ($this->offset) {
      $sql .= " OFFSET {$this->offset}";
    }
    return $sql;
  }
  protected function buildWhereSql(): string
  {
    $sql = '';
    foreach ($this->where as $index => $clause) {
      if (is_array($clause)) {
        $boolean = $index === 0 ? '' : $clause['boolean'] . ' ';
        $sql .= $boolean . $clause['condition'] . ' ';
      } else {
        // 문자열 형태일 경우 (like에서 사용됨)
        $boolean = $index === 0 ? '' : 'AND ';
        $sql .= $boolean . $clause . ' ';
      }
    }
    return trim($sql);
  }

  //Select부분분
  final public function select(array|string $columns): static
  {
    $this->select = is_array($columns) ? $columns : explode(',', $columns);
    return $this;
  }

  //Where절부분
  final public function where(array|string $column, mixed $value = null, ?string $operator = null, string $boolean = "AND"): static
  {
    if (is_array($column)) {
      foreach ($column as $col => $val) {
        $this->where($col, $val);  // 재귀 호출
      }
      return $this;
    }
    // where("col = NOW()") 형태의 raw 처리
    if ($value === null && $operator === null) {
      $this->where[] = ['condition' => $column, 'boolean' => $boolean];
      return $this;
    }
    // where("col", "val") → operator 생략 시 = 처리
    if ($operator === null) {
      $operator = "=";
    }
    $placeholder = ':w_' . count($this->bindings);
    $this->where[] = ['condition' => "$column $operator $placeholder", 'boolean' => $boolean];
    $this->bindings[$placeholder] = $value;
    return $this;
  }
  final public function orWhere(array|string $column, mixed $value = null, ?string $operator = null, string $boolean = "OR"): static
  {
    return $this->where($column, $value, $operator, $boolean);
  }
  final public function whereIn(string $column, array $values, string $boolean = 'AND', $conditon = "IN"): static
  {
    if (empty($values)) {
      throw new \InvalidArgumentException(__FUNCTION__ . ": values 배열이 비어있을 수 없습니다.");
    }
    $placeholders = [];
    foreach ($values as $index => $value) {
      $placeholder = ":in_" . count($this->bindings);
      $placeholders[] = $placeholder;
      $this->bindings[$placeholder] = $value;
    }
    $condition = "$column $conditon (" . implode(', ', $placeholders) . ")";
    $this->where[] = ['condition' => $condition, 'boolean' => $boolean];
    return $this;
  }
  final public function whereNotIn(string $column, array $values, $boolean = "AND", $conditon = "NOT IN"): static
  {
    if (empty($values)) {
      throw new \InvalidArgumentException(__FUNCTION__ . ": values 배열이 비어있을 수 없습니다.");
    }
    return $this->whereIn($column, $values, $boolean, $conditon);
  }
  final public function orWhereIn(string $column, array $values, $boolean = "OR", $conditon = "IN"): static
  {
    if (empty($values)) {
      throw new \InvalidArgumentException(__FUNCTION__ . ": values 배열이 비어있을 수 없습니다.");
    }
    return $this->whereIn($column, $values, $boolean, $conditon);
  }

  //Join
  final public function join(string $table, string $on, string $type = 'INNER'): static
  {
    $this->joins[] = strtoupper($type) . " JOIN $table ON $on";
    return $this;
  }

  //Like
  //사용예:
  // $model->like(['name' => '%홍%', 'email' => '%@naver.com%']);
  // $model->likeIn(['title', 'description'], '%공지%');
  // $model->orLikeIn(['name', 'nickname'], '%철수%');
  public function like(array|string $column, ?string $value = null, string $operator = 'LIKE', string $boolean = "AND"): static
  {
    $escapeClause = in_array(strtoupper($operator), ['LIKE', 'NOT LIKE']) ? " ESCAPE '\\\\'" : '';

    if (is_array($column)) {
      $conditions = [];
      foreach ($column as $col => $val) {
        $placeholder = ':l_' . count($this->bindings);
        $conditions[] = "$col $operator $placeholder$escapeClause";
        $this->bindings[$placeholder] = $val;
      }
      $this->where[] = ['condition' => '(' . implode(" $boolean ", $conditions) . ')', 'boolean' => $boolean];
    } else {
      $placeholder = ':l_' . count($this->bindings);
      $condition = "$column $operator $placeholder$escapeClause";
      $this->bindings[$placeholder] = $value;
      $this->where[] = ['condition' => $condition, 'boolean' => $boolean];
    }
    return $this;
  }
  public function orLike(array|string $column, ?string $value = null, string $operator = 'LIKE', string $boolean = "OR"): static
  {
    return $this->like($column, $value, $operator, $boolean);
  }
  public function notLike(array|string $column, ?string $value = null, string $operator = 'NOT LIKE', string $boolean = "AND"): static
  {
    return $this->like($column, $value, $operator, $boolean);
  }
  //likeIn 사용예:
  public function likeIn(array $columns, string $value, string $operator = "LIKE", string $boolean = "AND"): static
  {
    $escapeClause = in_array(strtoupper($operator), ['LIKE', 'NOT LIKE']) ? " ESCAPE '\\\\'" : '';
    $orConditions = [];

    foreach ($columns as $col) {
      $placeholder = ':li_' . count($this->bindings);
      $orConditions[] = "$col $operator $placeholder$escapeClause";
      $this->bindings[$placeholder] = $value;
    }

    $this->where[] = '(' . implode(" $boolean ", $orConditions) . ')';
    return $this;
  }
  public function orLikeIn(array $columns, string $value, string $operator = "LIKE", string $boolean = "OR"): static
  {
    return $this->likeIn($columns,  $value, $operator, $boolean);
  }
  public function notLikeIn(array $columns, string $value, string $operator = "NOT LIKE", string $boolean = "AND"): static
  {
    return $this->likeIn($columns,  $value, $operator, $boolean);
  }

  //Order
  final public function orderBy(mixed $column, string $direction = 'ASC'): static
  {
    if (is_array($column)) {
      // 배열 형식의 여러 컬럼 정렬을 처리
      foreach ($column as $col => $dir) {
        // 배열 키가 컬럼 이름이고 값이 방향임
        $this->order[] = "$col $dir";
      }
    } else {
      // 단일 컬럼에 대한 정렬
      $this->order[] = "$column $direction";
    }
    return $this;
  }
  //Limit부분
  final public function limit(int $limit): static
  {
    $this->limit = $limit;
    return $this;
  }
  final public function offset(int $offset): static
  {
    $this->offset = $offset;
    return $this;
  }

  //Customer SQL 실행부분
  final public function raw(string $sql, array $bindings = []): array
  {
    $stmt = $this->pdo->prepare($sql);
    foreach ($bindings as $key => $value) {
      $stmt->bindValue(is_int($key) ? $key + 1 : $key, $value);
    }
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }

  //Result부분
  //binding까지 완료한 SQL문을 return함
  final public function toRawSql(?string $select = null): string
  {
    $raw = $this->buildSelectSql($select);
    foreach ($this->bindings as $key => $value) {
      $escaped = is_numeric($value) ? $value : $this->pdo->quote($value);
      // 명명된 바인딩
      if (str_starts_with($key, ':')) {
        $raw = str_replace($key, $escaped, $raw);
      } else {
        // 물음표 바인딩인 경우 (whereIn 등)
        $raw = preg_replace('/\?/', $escaped, $raw, 1);
      }
    }
    return $raw;
  }
  private function execute(?string $select = null): PDOStatement
  {
    if ($this->_debug) {
      echo php_sapi_name() === 'cli'
        ? "\nSQL DEBUG: " . $this->toRawSql($select) . "\n"
        : "<pre>SQL DEBUG: " . $this->toRawSql($select) . "</pre>";
    }
    $this->latestQuery = $this->toRawSql();
    $stmt              = $this->pdo->prepare($this->buildSelectSql($select));
    foreach ($this->bindings as $key => $value) {
      $stmt->bindValue($key, $value);
    }
    $stmt->execute();
    return $stmt;
  }
  final public function count(string $select = "COUNT(*) as cnt", string $column = 'cnt'): int
  {
    $stmt = $this->execute($select);
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return (int)($result[$column] ?? 0);
  }
  final public function get(?string $select = null): mixed
  {
    $stmt = $this->execute($select);
    $this->where = [];
    $this->bindings = [];
    $result = $stmt->fetch(PDO::FETCH_ASSOC);
    return $result ?? null;
  }
  final public function getAll(?string $select = null): array
  {
    $stmt = $this->execute($select);
    $this->where = [];
    $this->bindings = [];
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
  }
  //CUD부분
  final public function insert(array $data): bool|int
  {
    $columns = [];
    $placeholders = [];
    $this->bindings = [];

    foreach ($data as $col => $val) {
      $safePlaceholder = ':i_' . count($this->bindings);
      if ($val === null) {
        // null 값인 경우 SQL 조각으로 처리
        $placeholders[] = $col;
      } else {
        $columns[] = $col;
        $placeholders[] = $safePlaceholder;
        $this->bindings[$safePlaceholder] = $val;
      }
    }

    $columnsSql = $columns ? '(' . implode(', ', $columns) . ')' : '';
    $placeholdersSql = '(' . implode(', ', $placeholders) . ')';

    $sql = "INSERT INTO {$this->table} {$columnsSql} VALUES {$placeholdersSql}";

    $stmt = $this->pdo->prepare($sql);
    foreach ($this->bindings as $key => $value) {
      $stmt->bindValue($key, $value);
    }

    $result = $stmt->execute();
    return $result && $stmt->rowCount() > 0 ? (int)$this->pdo->lastInsertId() : false;
  }

  final public function update(array $data): bool
  {
    if (empty($this->where)) {
      throw new \Exception(__FUNCTION__ . " 구문은 WHERE절 없이 사용할수 없습니다.");
    }

    $setParts = [];
    foreach ($data as $col => $val) {
      if ($val === null) {
        // 값이 null이면 $col을 SQL로 직접 사용
        $setParts[] = $col;
      } else {
        $placeholder = ':u_' . preg_replace('/\W+/', '_', $col); // 안전한 placeholder 이름
        $setParts[] = "$col = $placeholder";
        $this->bindings[$placeholder] = $val;
      }
    }

    $sql = "UPDATE {$this->table} SET " . implode(', ', $setParts)
      . " WHERE " . $this->buildWhereSql(); // <<< 수정된 부분

    $stmt = $this->pdo->prepare($sql);
    foreach ($this->bindings as $k => $v) {
      $stmt->bindValue($k, $v);
    }
    return $stmt->execute();
  }

  final public function delete(): bool
  {
    if (empty($this->where)) {
      throw new \Exception("DELETE 문에는 WHERE 절이 반드시 필요합니다.");
    }

    $sql = "DELETE FROM {$this->table} WHERE " . $this->buildWhereSql();

    $stmt = $this->pdo->prepare($sql);
    foreach ($this->bindings as $key => $value) {
      $stmt->bindValue($key, $value);
    }

    return $stmt->execute();
  }

  //transaction관련련
  final public function beginTransaction(): void
  {
    $this->pdo->beginTransaction();
  }

  final public function commit(): void
  {
    $this->pdo->commit();
  }

  final public function rollBack(): void
  {
    if ($this->pdo->inTransaction()) {
      $this->pdo->rollBack();
    }
  }
}
