#!/usr/bin/env bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
SCHEMA_ROOT="${SCHEMA_ROOT:-/home/ubuntu/springtime-schema-json}"
OUTPUT_ROOT="$PROJECT_DIR/class/json"

if [[ ! -d "$SCHEMA_ROOT" ]]; then
  echo "Schema root not found: $SCHEMA_ROOT" >&2
  exit 1
fi

mkdir -p "$OUTPUT_ROOT"

generate_php_from_schema() {
  local schema_path="$1"
  local relative_schema_path
  local stem
  local output_path

  relative_schema_path="$(realpath --relative-to="$SCHEMA_ROOT" "$schema_path")"
  if [[ "$relative_schema_path" == client-info/* ]]; then
    echo "Skipped $schema_path"
    return
  fi

  stem="$(basename "$schema_path")"
  stem="${stem%.schema.json}"
  output_path="$OUTPUT_ROOT/$stem.php"

  SCHEMA_PATH="$schema_path" OUTPUT_PATH="$output_path" STEM="$stem" php <<'PHP'
<?php
$schemaPath = getenv('SCHEMA_PATH');
$outputPath = getenv('OUTPUT_PATH');
$stem = getenv('STEM');

if ($schemaPath === false || $outputPath === false || $stem === false) {
    fwrite(STDERR, "Missing generator environment.\n");
    exit(1);
}

$schemaJson = file_get_contents($schemaPath);
if ($schemaJson === false) {
    fwrite(STDERR, "Unable to read schema: {$schemaPath}\n");
    exit(1);
}

$schema = json_decode($schemaJson, true);
if (!is_array($schema)) {
    fwrite(STDERR, "Invalid schema JSON: {$schemaPath}\n");
    exit(1);
}

$resolveLocalRef = static function (array $rootSchema, string $rootPath, string $refPath) {
  if (!str_starts_with($refPath, './')) {
    throw new RuntimeException("Unsupported ref path '{$refPath}'");
  }

  $targetPath = dirname($rootPath) . '/' . substr($refPath, 2);
  $targetJson = file_get_contents($targetPath);
  if ($targetJson === false) {
    throw new RuntimeException("Unable to read ref schema '{$targetPath}'");
  }

  $targetSchema = json_decode($targetJson, true);
  if (!is_array($targetSchema)) {
    throw new RuntimeException("Invalid ref schema JSON '{$targetPath}'");
  }

  return $targetSchema;
};

$className = 'json_' . preg_replace('/[^A-Za-z0-9_]/', '', $stem);
$lines = [];
$lines[] = '<?php';
$lines[] = '';

$buildObjectDtoLines = static function ($dtoClassName, $propertySchemas, $required) {
  $dtoLines = [];
  $propertyDeclarations = [];
  $propertyAssignments = [];
  $requiredChecks = [];
  $propertiesWithDefault = [];

  foreach ($propertySchemas as $propertyName => $propertySchema) {
    if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $propertyName)) {
      throw new RuntimeException("Unsupported property name '{$propertyName}'");
    }

    $exportedName = var_export($propertyName, true);
    $hasDefault = is_array($propertySchema)
      && array_key_exists('default', $propertySchema)
      && $propertySchema['default'] !== null;
    $defaultValue = $hasDefault ? var_export($propertySchema['default'], true) : 'null';
    $propertyDeclarations[] = "    public $$propertyName = $defaultValue;";
    $propertyAssignments[] = "        \$this->$propertyName = property_exists(\$decoded, $exportedName) ? \$decoded->$propertyName : $defaultValue;";

    if ($hasDefault) {
      $propertiesWithDefault[$propertyName] = true;
    }
  }

  foreach ($required as $propertyName) {
    $exportedName = var_export($propertyName, true);
    if (isset($propertiesWithDefault[$propertyName])) {
      $requiredChecks[] = "        if (property_exists(\$decoded, $exportedName) && \$decoded->$propertyName === null) {";
    } else {
      $requiredChecks[] = "        if (!property_exists(\$decoded, $exportedName) || \$decoded->$propertyName === null) {";
    }
    $requiredChecks[] = "            throw new InvalidArgumentException('Missing required field: $propertyName');";
    $requiredChecks[] = "        }";
  }

  if ($requiredChecks === []) {
    $requiredChecks[] = '        return;';
  }

  $dtoLines[] = 'class ' . $dtoClassName;
  $dtoLines[] = '{';

  foreach ($propertyDeclarations as $line) {
    $dtoLines[] = $line;
  }

  if ($propertyDeclarations !== []) {
    $dtoLines[] = '';
  }

  $dtoLines[] = '    public function __construct($data, $validateRequired = true)';
  $dtoLines[] = '    {';
  $dtoLines[] = '        $decoded = self::decodeInput($data);';
  $dtoLines[] = '        if ($validateRequired) {';
  $dtoLines[] = '            self::assertRequired($decoded);';
  $dtoLines[] = '        }';

  foreach ($propertyAssignments as $line) {
    $dtoLines[] = $line;
  }

  $dtoLines[] = '    }';
  $dtoLines[] = '';
  $dtoLines[] = '    public static function fromJson($data, $validateRequired = true)';
  $dtoLines[] = '    {';
  $dtoLines[] = '        return new self($data, $validateRequired);';
  $dtoLines[] = '    }';
  $dtoLines[] = '';
  $dtoLines[] = '    public static function fromData($data)';
  $dtoLines[] = '    {';
  $dtoLines[] = '        if ($data === null) {';
  $dtoLines[] = '            return null;';
  $dtoLines[] = '        }';
  $dtoLines[] = '';
  $dtoLines[] = '        if (is_array($data) && $data === array()) {';
  $dtoLines[] = '            return null;';
  $dtoLines[] = '        }';
  $dtoLines[] = '';
  $dtoLines[] = '        if (is_object($data) && get_object_vars($data) === array()) {';
  $dtoLines[] = '            return null;';
  $dtoLines[] = '        }';
  $dtoLines[] = '';
  $dtoLines[] = '        return new self($data, false);';
  $dtoLines[] = '    }';
  $dtoLines[] = '';
  $dtoLines[] = '    private static function decodeInput($data)';
  $dtoLines[] = '    {';
  $dtoLines[] = '        if (is_string($data)) {';
  $dtoLines[] = '            $decoded = json_decode($data);';
  $dtoLines[] = '            if (json_last_error() !== JSON_ERROR_NONE) {';
  $dtoLines[] = '                throw new InvalidArgumentException(' . var_export('Invalid JSON for ' . $dtoClassName . ': ', true) . ' . json_last_error_msg());';
  $dtoLines[] = '            }';
  $dtoLines[] = '        } elseif (is_array($data)) {';
  $dtoLines[] = '            $decoded = (object)$data;';
  $dtoLines[] = '        } elseif (is_object($data)) {';
  $dtoLines[] = '            $decoded = $data;';
  $dtoLines[] = '        } else {';
  $dtoLines[] = '            throw new InvalidArgumentException(' . var_export('Unsupported input type for ' . $dtoClassName, true) . ');';
  $dtoLines[] = '        }';
  $dtoLines[] = '';
  $dtoLines[] = '        if (!is_object($decoded)) {';
  $dtoLines[] = '            throw new InvalidArgumentException(' . var_export('Decoded payload for ' . $dtoClassName . ' must be an object', true) . ');';
  $dtoLines[] = '        }';
  $dtoLines[] = '';
  $dtoLines[] = '        return $decoded;';
  $dtoLines[] = '    }';
  $dtoLines[] = '';
  $dtoLines[] = '    private static function assertRequired($decoded)';
  $dtoLines[] = '    {';

  foreach ($requiredChecks as $line) {
    $dtoLines[] = $line;
  }

  $dtoLines[] = '    }';
  $dtoLines[] = '}';

  return $dtoLines;
};

$propertySchemas = is_array($schema['properties'] ?? null) ? $schema['properties'] : [];
$properties = array_keys($propertySchemas);
$required = array_values(array_filter($schema['required'] ?? [], 'is_string'));
$mapAdditionalProperties = $schema['additionalProperties'] ?? null;
$mapRefSchema = null;

if (is_array($mapAdditionalProperties) && is_string($mapAdditionalProperties['$ref'] ?? null)) {
  try {
    $mapRefSchema = $resolveLocalRef($schema, $schemaPath, $mapAdditionalProperties['$ref']);
  } catch (RuntimeException $e) {
    fwrite(STDERR, $e->getMessage() . " in {$schemaPath}\n");
    exit(1);
  }
}

$mapSchema = $schema['type'] === 'object'
  && $properties === []
  && (
    (
      is_array($mapAdditionalProperties)
      && ($mapAdditionalProperties['type'] ?? null) === 'object'
      && is_array($mapAdditionalProperties['properties'] ?? null)
    )
    || (
      is_array($mapRefSchema)
      && ($mapRefSchema['type'] ?? null) === 'object'
      && is_array($mapRefSchema['properties'] ?? null)
    )
  );

if ($mapSchema) {
  $entryClassName = $className . 'Entry';
  $entrySchema = is_array($mapRefSchema) ? $mapRefSchema : $mapAdditionalProperties;
  $entryPropertySchemas = $entrySchema['properties'];
  $entryRequired = array_values(array_filter($entrySchema['required'] ?? [], 'is_string'));
  $propertyNamePattern = is_array($schema['propertyNames'] ?? null) ? ($schema['propertyNames']['pattern'] ?? null) : null;

  try {
    foreach ($buildObjectDtoLines($entryClassName, $entryPropertySchemas, $entryRequired) as $line) {
      $lines[] = $line;
    }
  } catch (RuntimeException $e) {
    fwrite(STDERR, $e->getMessage() . " in {$schemaPath}\n");
    exit(1);
  }

  $lines[] = '';
  $lines[] = 'class ' . $className;
  $lines[] = '{';
  $lines[] = '    public static function fromJson($data, $validateRequired = true)';
  $lines[] = '    {';
  $lines[] = '        $decoded = self::decodeInput($data);';
  $lines[] = '        return self::mapEntries($decoded, $validateRequired);';
  $lines[] = '    }';
  $lines[] = '';
  $lines[] = '    public static function fromData($data)';
  $lines[] = '    {';
  $lines[] = '        if ($data === null) {';
  $lines[] = '            return null;';
  $lines[] = '        }';
  $lines[] = '';
  $lines[] = '        if (is_array($data) && $data === array()) {';
  $lines[] = '            return null;';
  $lines[] = '        }';
  $lines[] = '';
  $lines[] = '        if (is_object($data) && get_object_vars($data) === array()) {';
  $lines[] = '            return null;';
  $lines[] = '        }';
  $lines[] = '';
  $lines[] = '        $decoded = self::decodeInput($data);';
  $lines[] = '        return self::mapEntries($decoded, false);';
  $lines[] = '    }';
  $lines[] = '';
  $lines[] = '    private static function decodeInput($data)';
  $lines[] = '    {';
  $lines[] = '        if (is_string($data)) {';
  $lines[] = '            $decoded = json_decode($data);';
  $lines[] = '            if (json_last_error() !== JSON_ERROR_NONE) {';
  $lines[] = '                throw new InvalidArgumentException(' . var_export('Invalid JSON for ' . $className . ': ', true) . ' . json_last_error_msg());';
  $lines[] = '            }';
  $lines[] = '        } elseif (is_array($data)) {';
  $lines[] = '            $decoded = (object)$data;';
  $lines[] = '        } elseif (is_object($data)) {';
  $lines[] = '            $decoded = $data;';
  $lines[] = '        } else {';
  $lines[] = '            throw new InvalidArgumentException(' . var_export('Unsupported input type for ' . $className, true) . ');';
  $lines[] = '        }';
  $lines[] = '';
  $lines[] = '        if (!is_object($decoded)) {';
  $lines[] = '            throw new InvalidArgumentException(' . var_export('Decoded payload for ' . $className . ' must be an object', true) . ');';
  $lines[] = '        }';
  $lines[] = '';
  $lines[] = '        return $decoded;';
  $lines[] = '    }';
  $lines[] = '';
  $lines[] = '    private static function mapEntries($decoded, $validateRequired)';
  $lines[] = '    {';
  if (is_string($propertyNamePattern) && $propertyNamePattern !== '') {
    $patternLiteral = var_export('/' . str_replace('/', '\\/', $propertyNamePattern) . '/', true);
    $lines[] = '        foreach (array_keys(get_object_vars($decoded)) as $key) {';
    $lines[] = '            if ($validateRequired && !preg_match(' . $patternLiteral . ', (string)$key)) {';
    $lines[] = '                throw new InvalidArgumentException(' . var_export('Invalid map key for ' . $className . ': ', true) . ' . $key);';
    $lines[] = '            }';
    $lines[] = '        }';
    $lines[] = '';
  }
  $lines[] = '        $result = new stdClass();';
  $lines[] = '        foreach (get_object_vars($decoded) as $key => $value) {';
  $lines[] = '            $result->$key = ' . $entryClassName . '::fromJson($value, $validateRequired);';
  $lines[] = '        }';
  $lines[] = '';
  $lines[] = '        return $result;';
  $lines[] = '    }';
  $lines[] = '}';
} else {
  try {
    foreach ($buildObjectDtoLines($className, $propertySchemas, $required) as $line) {
      $lines[] = $line;
    }
  } catch (RuntimeException $e) {
    fwrite(STDERR, $e->getMessage() . " in {$schemaPath}\n");
    exit(1);
  }
}

$lines[] = '';
$lines[] = '?>';

$result = file_put_contents($outputPath, implode(PHP_EOL, $lines));
if ($result === false) {
    fwrite(STDERR, "Unable to write generated PHP: {$outputPath}\n");
    exit(1);
}
PHP

  echo "Generated $output_path"
}

if [[ $# -gt 0 ]]; then
  for schema_arg in "$@"; do
    if [[ -f "$schema_arg" ]]; then
      generate_php_from_schema "$(realpath "$schema_arg")"
    elif [[ -f "$SCHEMA_ROOT/$schema_arg" ]]; then
      generate_php_from_schema "$(realpath "$SCHEMA_ROOT/$schema_arg")"
    else
      mapfile -d '' schema_matches < <(find "$SCHEMA_ROOT" -type f -name "$schema_arg" -print0)
      if [[ ${#schema_matches[@]} -eq 1 ]]; then
        generate_php_from_schema "${schema_matches[0]}"
      elif [[ ${#schema_matches[@]} -gt 1 ]]; then
        echo "Multiple schema files found for: $schema_arg" >&2
        exit 1
      else
        echo "Schema file not found: $schema_arg" >&2
        exit 1
      fi
    fi
  done
  exit 0
fi

found_schema=false
while IFS= read -r -d '' schema_path; do
  found_schema=true
  generate_php_from_schema "$schema_path"
done < <(find "$SCHEMA_ROOT" -type f -name '*.schema.json' -print0 | sort -z)

if [[ "$found_schema" == false ]]; then
  echo "No schema files found under $SCHEMA_ROOT" >&2
  exit 1
fi