package com.jacoby6000.smithplates.sql.model

import software.amazon.smithy.model.shapes.ShapeId

sealed trait SqlSchemaError {
  def message: String
}

final case class InvalidPluginConfig(message: String) extends SqlSchemaError

final case class InvalidMemberColumnType(
    memberName: String,
    targetShape: ShapeId,
    table: String,
    kind: InvalidMemberColumnType.Kind,
    detail: Option[String] = None
) extends SqlSchemaError {
  override def message: String = kind match {
    case InvalidMemberColumnType.Kind.Unsupported     =>
      s"Unsupported SQL column target ${targetShape.toString} for table '$table' member '$memberName'"
    case InvalidMemberColumnType.Kind.SqlVarchar      =>
      s"@sqlVarchar on table '$table' member '$memberName' requires a string target, got ${targetShape.toString}"
    case InvalidMemberColumnType.Kind.SqlUuid         =>
      s"@sqlUuid on table '$table' member '$memberName' requires a string target, got ${targetShape.toString}"
    case InvalidMemberColumnType.Kind.SqlJson         =>
      s"@sqlJson on table '$table' member '$memberName' requires a list, map, structure, union, or document target, got ${targetShape.toString}"
    case InvalidMemberColumnType.Kind.TimestampFormat =>
      detail.getOrElse(
        s"Unsupported @timestampFormat on table '$table' member '$memberName'"
      )
  }
}

object InvalidMemberColumnType {
  enum Kind {
    case Unsupported
    case SqlVarchar
    case SqlUuid
    case SqlJson
    case TimestampFormat
  }
}

final case class MissingSqlTableName(shape: ShapeId) extends SqlSchemaError {
  override def message: String = s"Missing sqlTable.name on structure ${shape.toString}"
}

case object NoSqlTables extends SqlSchemaError {
  override def message: String = "Smithy model contains no @sqlTable structures"
}

final case class ConflictingSqlIndexTraits(shape: ShapeId, memberName: String) extends SqlSchemaError {
  override def message: String =
    s"sqlIndex and sqlUniqueIndex on ${shape.toString} member '$memberName' are mutually exclusive"
}

final case class DuplicateSqlColumnIndex(
    shape: ShapeId,
    memberNames: List[String],
    index: Int
) extends SqlSchemaError {
  override def message: String =
    s"Duplicate @sqlColumnIndex value $index on ${shape.toString} members ${memberNames.mkString(", ")}"
}

final case class InvalidForeignKeyReference(
    shape: ShapeId,
    memberName: String,
    reference: String
) extends SqlSchemaError {
  override def message: String =
    s"Invalid foreign key reference '$reference' on ${shape.toString} member '$memberName'"
}

final case class MissingPrimaryKey(shape: ShapeId, tableName: String) extends SqlSchemaError {
  override def message: String =
    s"Structure ${shape.toString} mapped to table '$tableName' has no @sqlPrimaryKey member"
}

final case class InvalidQueryTableReference(
    shape: ShapeId,
    tableRef: String,
    queryKind: InvalidQueryTableReference.Kind
) extends SqlSchemaError {
  override def message: String =
    s"${queryKind.label} on ${shape.toString} references invalid @sqlTable '$tableRef'"
}

object InvalidQueryTableReference {
  enum Kind {
    case Insert
    case DeriveUpdate
    case DeriveDelete
    case DeriveSelectOne
    case DeriveSelect
    case Update

    def label: String =
      this match {
        case Insert          => "sqlDeriveInsert"
        case DeriveUpdate    => "sqlDeriveUpdate"
        case DeriveDelete    => "sqlDeriveDelete"
        case DeriveSelectOne => "sqlDeriveSelectOne"
        case DeriveSelect    => "sqlDeriveSelect"
        case Update          => "sqlUpdate"
      }
  }
}

final case class QueryMemberNotOnTable(
    shape: ShapeId,
    memberName: String,
    tableName: String,
    queryKind: InvalidQueryTableReference.Kind
) extends SqlSchemaError {
  override def message: String =
    s"${queryKind.label} on ${shape.toString} member '$memberName' is not a member of table '$tableName'"
}

final case class QueryMissingRequiredMember(
    shape: ShapeId,
    memberName: String,
    tableName: String,
    queryKind: InvalidQueryTableReference.Kind
) extends SqlSchemaError {
  override def message: String =
    s"${queryKind.label} on ${shape.toString} is missing required table '$tableName' member '$memberName'"
}

final case class QueryIncludesAutoGeneratedMember(
    shape: ShapeId,
    memberName: String,
    tableName: String,
    queryKind: InvalidQueryTableReference.Kind
) extends SqlSchemaError {
  override def message: String =
    s"${queryKind.label} on ${shape.toString} must not include database-managed table '$tableName' member '$memberName'"
}

final case class MissingJoinForeignKey(
    shape: ShapeId,
    deriveTrait: String,
    primaryTable: String,
    joinTable: String
) extends SqlSchemaError {
  override def message: String =
    s"$deriveTrait on ${shape.toString} cannot join '$joinTable' to '$primaryTable': no @sqlForeignKey relationship"
}

final case class AmbiguousJoinForeignKey(
    shape: ShapeId,
    deriveTrait: String,
    primaryTable: String,
    joinTable: String
) extends SqlSchemaError {
  override def message: String =
    s"$deriveTrait on ${shape.toString} cannot join '$joinTable' to '$primaryTable': multiple @sqlForeignKey relationships"
}

final case class InvalidSelectColumn(
    shape: ShapeId,
    memberName: String,
    tableRef: String,
    column: String
) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} member '$memberName' references invalid column '$column' on table '$tableRef'"
}

final case class AmbiguousSelectColumnMember(
    shape: ShapeId,
    memberName: String,
    tableRefs: List[String]
) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} member '$memberName' matches table members on ${tableRefs.mkString(", ")}; add tableRef/column in @sqlDeriveSelect or @sqlColumn(name: \"...\") to disambiguate"
}

final case class UnresolvedSelectColumnMember(shape: ShapeId, memberName: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} member '$memberName' does not match any table member; add tableRef/column in @sqlDeriveSelect or @sqlColumn(name: \"...\") when the physical column is unique"
}

final case class InvalidWhereColumn(shape: ShapeId, memberName: String, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} where member '$memberName' is invalid: $reason"
}

final case class InvalidHavingColumn(shape: ShapeId, memberName: String, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} having member '$memberName' is invalid: $reason"
}

final case class InvalidGroupByColumn(
    shape: ShapeId,
    memberName: String,
    tableRef: String,
    column: String
) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} groupBy member '$memberName' references invalid column '$column' on table '$tableRef'"
}

final case class InvalidJoinType(shape: ShapeId, joinType: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} has unsupported join type '$joinType'"
}

final case class SelectGroupByMissingColumn(shape: ShapeId, selectMemberName: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} groupBy is missing projection '$selectMemberName'"
}

final case class MissingSqlServiceVersion(shape: ShapeId) extends SqlSchemaError {
  override def message: String =
    s"sqlService on ${shape.toString} requires a non-empty service version"
}

final case class EmptySqlService(shape: ShapeId) extends SqlSchemaError {
  override def message: String =
    s"sqlService on ${shape.toString} must declare at least one operation"
}

final case class InvalidSqlService(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlService on ${shape.toString} is invalid: $reason"
}

final case class InvalidDeriveUpdate(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveUpdate on ${shape.toString} is invalid: $reason"
}

final case class InvalidDeriveDelete(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveDelete on ${shape.toString} is invalid: $reason"
}

final case class InvalidDeriveSelectOne(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelectOne on ${shape.toString} is invalid: $reason"
}

final case class InvalidDeriveSelect(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveSelect on ${shape.toString} is invalid: $reason"
}

final case class InvalidDeriveInsert(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"sqlDeriveInsert on ${shape.toString} is invalid: $reason"
}

final case class InvalidSqlOperation(serviceShape: ShapeId, operationName: String, reason: String)
    extends SqlSchemaError {
  override def message: String =
    s"sqlService on ${serviceShape.toString} operation '$operationName' is invalid: $reason"
}

final case class InvalidCodegenShape(shape: ShapeId, reason: String) extends SqlSchemaError {
  override def message: String =
    s"SQL service codegen shape ${shape.toString} is invalid: $reason"
}
