import type {
  Poi,
  PoiCategory,
  PoiDataset,
  PoiDatasetValidationResult,
  PoiImageAsset,
  PoiOpeningHours,
  PoiTag,
  PoiValidationErrorCode,
  PoiValidationIssue,
} from './types'

// A conservative POC envelope used to catch obviously misplaced coordinates.
// Content review remains the authority for whether a point belongs to Guangming District.
/**
 * 光明区 POC 的告警范围，用于提示「这个点位可能不在光明区」。
 *
 * 注意：这是内容审核的告警包络，不是运行时的坐标限制。真实用户可能站在范围之外
 * （比如在市区规划行程），不能拿它去拒绝定位结果 —— 那是 isTrustworthyCoordinate 的活。
 */
export const GUANGMING_POC_BOUNDS = {
  minLongitude: 113.78,
  maxLongitude: 114.02,
  minLatitude: 22.70,
  maxLatitude: 22.89,
} as const

const ISO_DATE_TIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/
const HTTPS_URL_PATTERN = /^https:\/\//i
const LOCAL_ASSET_PATTERN = /^\/static\//
const NEARBY_POI_WARNING_METERS = 30

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

/**
 * 运行时坐标可信判断，用于点位数据校验与地图/定位状态写入。
 *
 * 除了有限性和全球范围，还必须拒绝经纬度为 0 的坐标：
 * 地图 SDK 尚未拿到定位、或定位接口降级时会回传 (0, 0)，
 * 它落在几内亚湾（Null Island）。微信 `<map>` 在境外坐标下会切到 HERE 底图，
 * 视觉上就是「地图跑到非洲的海里」，还会因为境外瓦片加载失败而留下大片空白。
 */
export function isTrustworthyCoordinate(longitude: unknown, latitude: unknown): boolean {
  return typeof longitude === 'number'
    && typeof latitude === 'number'
    && Number.isFinite(longitude)
    && Number.isFinite(latitude)
    && longitude !== 0
    && latitude !== 0
    && longitude >= -180
    && longitude <= 180
    && latitude >= -90
    && latitude <= 90
}

function isNonEmptyString(value: unknown): value is string {
  return typeof value === 'string' && value.trim().length > 0
}

function isIsoDateTime(value: unknown): value is string {
  return typeof value === 'string' && ISO_DATE_TIME_PATTERN.test(value) && Number.isFinite(Date.parse(value))
}

function hasDuplicates(values: readonly string[]): boolean {
  return new Set(values).size !== values.length
}

function addIssue(
  issues: PoiValidationIssue[],
  code: PoiValidationErrorCode,
  path: string,
  message: string,
  poiId?: string,
  severity: PoiValidationIssue['severity'] = 'error',
): void {
  issues.push({ code, path, message, poiId, severity })
}

function validateRootStructure(dataset: unknown, issues: PoiValidationIssue[]): dataset is PoiDataset {
  if (!isRecord(dataset)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', '$', '数据包必须是对象')
    return false
  }

  if (!isRecord(dataset.meta)
    || !Array.isArray(dataset.categories)
    || !Array.isArray(dataset.tags)
    || !Array.isArray(dataset.images)
    || !Array.isArray(dataset.pois)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', '$', '数据包缺少 meta、categories、tags、images 或 pois')
    return false
  }

  if (!dataset.categories.every(isRecord)
    || !dataset.tags.every(isRecord)
    || !dataset.images.every(isRecord)
    || !dataset.pois.every(isRecord)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', '$', '数据包列表成员必须是对象')
    return false
  }

  if (!isNonEmptyString(dataset.meta.datasetVersion)
    || !isIsoDateTime(dataset.meta.generatedAt)
    || !Array.isArray(dataset.meta.reviewedBy)
    || dataset.meta.reviewedBy.length === 0
    || !dataset.meta.reviewedBy.every(isNonEmptyString)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', 'meta', '数据版本、生成时间和审核人信息必须完整')
  }

  if (dataset.meta.coverageScope !== 'reviewed_sample'
    && dataset.meta.coverageScope !== 'authoritative_baseline') {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', 'meta.coverageScope', '覆盖口径无效')
  }

  if (dataset.meta.coverageScope === 'reviewed_sample'
    && (dataset.meta.authoritativeBaselineProvided || dataset.meta.canClaimFullCoverage)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', 'meta.canClaimFullCoverage', '审核样本不得声称全域覆盖')
  }

  if (dataset.meta.coverageScope === 'authoritative_baseline'
    && (!dataset.meta.authoritativeBaselineProvided || !dataset.meta.canClaimFullCoverage)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', 'meta.authoritativeBaselineProvided', '全域口径必须有权威冻结基准清单')
  }

  const viewport = dataset.meta.defaultViewport
  if (!isRecord(viewport)
    || typeof viewport.longitude !== 'number'
    || !Number.isFinite(viewport.longitude)
    || typeof viewport.latitude !== 'number'
    || !Number.isFinite(viewport.latitude)
    || typeof viewport.scale !== 'number'
    || !Number.isFinite(viewport.scale)
    || viewport.scale <= 0) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', 'meta.defaultViewport', '默认地图视野无效')
  }

  return !issues.some(issue => issue.code === 'DATASET_STRUCTURE_INVALID')
}

function validateDictionary<T extends PoiCategory | PoiTag>(
  items: readonly T[],
  dictionaryName: 'categories' | 'tags',
  issues: PoiValidationIssue[],
): void {
  const codes = items.map(item => item.code)
  if (items.length === 0 || codes.some(code => !isNonEmptyString(code)) || hasDuplicates(codes)) {
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', dictionaryName, `${dictionaryName} 必须包含不重复的有效编码`)
  }

  items.forEach((item, index) => {
    if (!isNonEmptyString(item.name)
      || !Number.isInteger(item.sortOrder)
      || (item.status !== 'enabled' && item.status !== 'disabled')) {
      addIssue(issues, 'DATASET_STRUCTURE_INVALID', `${dictionaryName}[${index}]`, '字典记录结构无效')
    }

    if (dictionaryName === 'categories') {
      const category = item as PoiCategory
      if (!isNonEmptyString(category.markerIcon) || !isNonEmptyString(category.selectedMarkerIcon))
        addIssue(issues, 'DATASET_STRUCTURE_INVALID', `${dictionaryName}[${index}]`, '分类 marker 图标不完整')
    }

    if (dictionaryName === 'tags') {
      const tag = item as PoiTag
      if (tag.synonyms !== undefined
        && (!Array.isArray(tag.synonyms) || !tag.synonyms.every(isNonEmptyString))) {
        addIssue(issues, 'DATASET_STRUCTURE_INVALID', `${dictionaryName}[${index}].synonyms`, '标签同义词必须是非空字符串列表')
      }
    }
  })
}

function validateImages(images: readonly PoiImageAsset[], issues: PoiValidationIssue[]): void {
  const ids = images.map(image => image.id)
  if (hasDuplicates(ids))
    addIssue(issues, 'DATASET_STRUCTURE_INVALID', 'images', '图片资产 ID 不得重复')

  images.forEach((image, index) => {
    const path = `images[${index}]`
    if (!isNonEmptyString(image.id)
      || !isNonEmptyString(image.url)
      || (!HTTPS_URL_PATTERN.test(image.url) && !LOCAL_ASSET_PATTERN.test(image.url))
      || !isNonEmptyString(image.source)
      || !isNonEmptyString(image.licenseEvidence)
      || !isNonEmptyString(image.reviewedBy)
      || !isIsoDateTime(image.reviewedAt)
      || !isIsoDateTime(image.updatedAt)
      || !isNonEmptyString(image.alt)) {
      addIssue(issues, 'IMAGE_INVALID', path, '图片资产字段、路径或审核信息无效')
    }
    if (image.copyrightStatus !== 'cleared')
      addIssue(issues, 'COPYRIGHT_NOT_CLEARED', `${path}.copyrightStatus`, '图片使用权未确认')
    if (image.isPlaceholder)
      addIssue(issues, 'POC_PLACEHOLDER_ASSET', path, '当前为 POC 占位图，不计入正式图片验收', undefined, 'warning')
  })
}

function validateOpeningHours(openingHours: PoiOpeningHours, poiId: string, issues: PoiValidationIssue[]): void {
  const path = `pois.${poiId}.openingHours`
  const validStatuses = ['known', 'unknown', 'by_appointment', 'temporarily_closed']
  if (!isRecord(openingHours) || !validStatuses.includes(openingHours.status)) {
    addIssue(issues, 'OPENING_HOURS_INVALID', path, '开放时间状态无效', poiId)
    return
  }

  if (openingHours.note !== null && !isNonEmptyString(openingHours.note))
    addIssue(issues, 'OPENING_HOURS_INVALID', `${path}.note`, '开放时间备注必须为非空字符串或 null', poiId)

  if (openingHours.status === 'unknown') {
    if (openingHours.displayText !== null || openingHours.sourceUpdatedAt !== null)
      addIssue(issues, 'OPENING_HOURS_INVALID', path, '开放时间未知时展示文案和来源时间应为 null', poiId)
    return
  }

  if (!isNonEmptyString(openingHours.displayText) || !isIsoDateTime(openingHours.sourceUpdatedAt))
    addIssue(issues, 'OPENING_HOURS_INVALID', path, '已知、预约或临时关闭状态必须有展示文案和来源更新时间', poiId)
}

function validateCoordinates(poi: Poi, issues: PoiValidationIssue[]): void {
  const { longitude, latitude } = poi
  const id = isNonEmptyString(poi.id) ? poi.id : 'invalid_poi'
  if (!isTrustworthyCoordinate(longitude, latitude)) {
    addIssue(issues, 'COORDINATE_INVALID', `pois.${id}.coordinates`, '经纬度必须为非零的有限合法数值', id)
    return
  }

  if (poi.coordinateSystem !== 'GCJ02')
    addIssue(issues, 'COORDINATE_SYSTEM_INVALID', `pois.${id}.coordinateSystem`, '本期坐标系必须为 GCJ02', id)

  if (!isWithinGuangmingArea(longitude, latitude))
    addIssue(issues, 'OUTSIDE_TARGET_AREA', `pois.${id}.coordinates`, '坐标超出光明区 POC 粗校验包络，需内容负责人复核', id)
}

/**
 * 坐标是否落在光明区 POC 粗校验包络内。
 *
 * 这是一个宽松的矩形包络，不是行政边界，也**不该**用来拒绝真实定位 ——
 * 用户站在包络外面是完全正常的。它只回答一个问题：把地图移到这个坐标，
 * 画面里还看得见本期点位吗？用途有两个：数据入库时的内容审核告警，
 * 以及开屏定位时判断该不该把视野从聚类中心移到用户身上。
 */
export function isWithinGuangmingArea(longitude: number, latitude: number): boolean {
  return longitude >= GUANGMING_POC_BOUNDS.minLongitude
    && longitude <= GUANGMING_POC_BOUNDS.maxLongitude
    && latitude >= GUANGMING_POC_BOUNDS.minLatitude
    && latitude <= GUANGMING_POC_BOUNDS.maxLatitude
}

function calculateDistanceMeters(left: Poi, right: Poi): number {
  const earthRadiusMeters = 6371000
  const toRadians = (degrees: number) => degrees * Math.PI / 180
  const latitudeDelta = toRadians(right.latitude - left.latitude)
  const longitudeDelta = toRadians(right.longitude - left.longitude)
  const leftLatitude = toRadians(left.latitude)
  const rightLatitude = toRadians(right.latitude)
  const haversine = Math.sin(latitudeDelta / 2) ** 2
    + Math.cos(leftLatitude) * Math.cos(rightLatitude) * Math.sin(longitudeDelta / 2) ** 2

  return earthRadiusMeters * 2 * Math.asin(Math.sqrt(haversine))
}

function warnAboutNearbyPois(pois: readonly Poi[], issues: PoiValidationIssue[]): void {
  for (let leftIndex = 0; leftIndex < pois.length; leftIndex += 1) {
    const left = pois[leftIndex]
    if (!Number.isFinite(left.longitude) || !Number.isFinite(left.latitude))
      continue

    for (let rightIndex = leftIndex + 1; rightIndex < pois.length; rightIndex += 1) {
      const right = pois[rightIndex]
      if (!Number.isFinite(right.longitude) || !Number.isFinite(right.latitude))
        continue

      const distanceMeters = calculateDistanceMeters(left, right)
      if (distanceMeters < NEARBY_POI_WARNING_METERS) {
        addIssue(
          issues,
          'POTENTIAL_COORDINATE_OVERLAP',
          `pois.${right.id}.coordinates`,
          `与 ${left.id} 相距约 ${Math.round(distanceMeters)} 米，需复核是否重复或存在 marker 遮挡`,
          right.id,
          'warning',
        )
      }
    }
  }
}

function validatePoi(
  poi: Poi,
  categories: ReadonlyMap<string, PoiCategory>,
  tags: ReadonlyMap<string, PoiTag>,
  images: ReadonlyMap<string, PoiImageAsset>,
  issues: PoiValidationIssue[],
  poiIndex: number,
): void {
  const issuePoiId = isNonEmptyString(poi.id) ? poi.id : `__invalid_index_${poiIndex}`
  const requiredStrings: Array<[keyof Poi, unknown]> = [
    ['id', poi.id],
    ['name', poi.name],
    ['categoryCode', poi.categoryCode],
    ['coverImageId', poi.coverImageId],
    ['summary', poi.summary],
    ['description', poi.description],
    ['address', poi.address],
    ['recommendationConfiguredBy', poi.recommendationConfiguredBy],
    ['recommendationUpdatedAt', poi.recommendationUpdatedAt],
    ['dataSource', poi.dataSource],
    ['contentRightsEvidence', poi.contentRightsEvidence],
    ['updatedAt', poi.updatedAt],
  ]
  requiredStrings.forEach(([field, value]) => {
    if (!isNonEmptyString(value))
      addIssue(issues, 'REQUIRED_FIELD_MISSING', `pois.${issuePoiId}.${String(field)}`, '必填字段缺失', issuePoiId)
  })

  const category = categories.get(poi.categoryCode)
  if (!category || category.status !== 'enabled')
    addIssue(issues, 'CATEGORY_INVALID', `pois.${issuePoiId}.categoryCode`, '分类不存在或已禁用', issuePoiId)

  if (!Array.isArray(poi.tagCodes)
    || poi.tagCodes.length < 1
    || poi.tagCodes.length > 5
    || hasDuplicates(poi.tagCodes)
    || poi.tagCodes.some(tagCode => tags.get(tagCode)?.status !== 'enabled')) {
    addIssue(issues, 'TAG_INVALID', `pois.${issuePoiId}.tagCodes`, '必须引用 1-5 个不重复的启用标签', issuePoiId)
  }

  validateCoordinates(poi, issues)

  const detailImageIds = Array.isArray(poi.imageIds) ? poi.imageIds : []
  const allImageIds = [poi.coverImageId, ...detailImageIds]
  if (!Array.isArray(poi.imageIds)
    || poi.imageIds.length > 8
    || hasDuplicates(allImageIds)
    || allImageIds.some(imageId => !images.has(imageId))) {
    addIssue(issues, 'IMAGE_INVALID', `pois.${issuePoiId}.imageIds`, '图片引用不存在、重复或超过 8 张详情图', issuePoiId)
  }
  if (allImageIds.some(imageId => images.get(imageId)?.copyrightStatus !== 'cleared'))
    addIssue(issues, 'COPYRIGHT_NOT_CLEARED', `pois.${issuePoiId}.imageIds`, '引用了使用权未确认的图片', issuePoiId)

  validateOpeningHours(poi.openingHours, issuePoiId, issues)

  if (!Number.isInteger(poi.recommendationIndex)
    || poi.recommendationIndex < 1
    || poi.recommendationIndex > 5
    || !['official_editorial', 'poc_default'].includes(poi.recommendationSource)
    || !isIsoDateTime(poi.recommendationUpdatedAt)) {
    addIssue(issues, 'RECOMMENDATION_INVALID', `pois.${issuePoiId}.recommendationIndex`, '推荐指数必须是 1-5 整数、使用受控来源且含更新信息', issuePoiId)
  }

  if (poi.contentRightsStatus !== 'cleared')
    addIssue(issues, 'COPYRIGHT_NOT_CLEARED', `pois.${issuePoiId}.contentRightsStatus`, '内容使用权未确认', issuePoiId)
  if (poi.reviewStatus !== 'approved' || !isNonEmptyString(poi.reviewedBy) || !isIsoDateTime(poi.reviewedAt))
    addIssue(issues, 'REVIEW_NOT_APPROVED', `pois.${issuePoiId}.reviewStatus`, '内容尚未完成审核', issuePoiId)
  if (poi.publishStatus !== 'published')
    addIssue(issues, 'PUBLISH_STATUS_INVALID', `pois.${issuePoiId}.publishStatus`, '只有 published 记录才能进入前台', issuePoiId)
  if (!isIsoDateTime(poi.updatedAt))
    addIssue(issues, 'REQUIRED_FIELD_MISSING', `pois.${issuePoiId}.updatedAt`, '更新时间必须是 ISO UTC 时间', issuePoiId)
}

export function validatePoiDataset(dataset: unknown): PoiDatasetValidationResult {
  const issues: PoiValidationIssue[] = []
  if (!validateRootStructure(dataset, issues)) {
    return {
      isDatasetUsable: false,
      isValid: false,
      issues,
      validPois: [],
      invalidPoiIds: [],
    }
  }

  validateDictionary(dataset.categories, 'categories', issues)
  validateDictionary(dataset.tags, 'tags', issues)
  validateImages(dataset.images, issues)

  const poiIds = dataset.pois.map(poi => poi.id)
  if (hasDuplicates(poiIds))
    addIssue(issues, 'DUPLICATE_ID', 'pois', 'POI ID 不得重复')

  const categoryMap = new Map(dataset.categories.map(category => [category.code, category]))
  const tagMap = new Map(dataset.tags.map(tag => [tag.code, tag]))
  const imageMap = new Map(dataset.images.map(image => [image.id, image]))
  dataset.pois.forEach((poi, index) => validatePoi(poi, categoryMap, tagMap, imageMap, issues, index))
  warnAboutNearbyPois(dataset.pois, issues)

  const structuralError = issues.some(issue => issue.severity === 'error'
    && (issue.code === 'DATASET_STRUCTURE_INVALID'
      || issue.code === 'DUPLICATE_ID'
      || (!issue.poiId && issue.code !== 'POC_PLACEHOLDER_ASSET')))

  if (structuralError) {
    return {
      isDatasetUsable: false,
      isValid: false,
      issues,
      validPois: [],
      invalidPoiIds: [],
    }
  }

  const invalidPoiIds = Array.from(new Set(issues
    .filter(issue => issue.severity === 'error' && issue.poiId)
    .map(issue => issue.poiId as string)))
  const invalidPoiIdSet = new Set(invalidPoiIds)
  const validPois = dataset.pois.filter((poi, index) => !invalidPoiIdSet.has(
    isNonEmptyString(poi.id) ? poi.id : `__invalid_index_${index}`,
  ))

  return {
    isDatasetUsable: true,
    isValid: issues.every(issue => issue.severity !== 'error'),
    issues,
    validPois,
    invalidPoiIds,
  }
}

export function assertUsablePoiDataset(dataset: unknown): asserts dataset is PoiDataset {
  const result = validatePoiDataset(dataset)
  if (!result.isDatasetUsable) {
    const summary = result.issues
      .filter(issue => issue.severity === 'error')
      .map(issue => `${issue.code}@${issue.path}`)
      .join(', ')
    throw new Error(`POI dataset is unusable: ${summary}`)
  }
}
