use crate::Declaration;
use crate::html::DiffStatus;
use crate::html::DocNodeWithContext;
use crate::html::parameters::render_params;
use crate::html::render_context::RenderContext;
use crate::html::types::render_type_def_colon;
use crate::html::types::type_params_summary;
use crate::html::util::*;

pub(crate) fn render_interface(
  ctx: &RenderContext,
  symbol: &DocNodeWithContext,
  decl: &Declaration,
) -> Vec<SectionCtx> {
  let interface_def = decl.interface_def().unwrap();
  let name = symbol.get_qualified_name();

  let current_type_params = interface_def
    .type_params
    .iter()
    .map(|def| def.name.as_str())
    .collect::<std::collections::HashSet<&str>>();
  let ctx = &ctx.with_current_type_params(current_type_params);

  let interface_diff = ctx.ctx.diff.as_ref().and_then(|diff_index| {
    diff_index
      .get_def_diff(
        &symbol.origin.specifier,
        symbol.get_name(),
        decl.def.to_kind(),
      )
      .and_then(|d| d.as_interface())
  });

  let mut sections = vec![];

  if let Some(type_params) = crate::html::types::render_type_params(
    ctx,
    &decl.js_doc,
    &interface_def.type_params,
    &decl.location,
    interface_diff.and_then(|d| d.type_params_change.as_ref()),
  ) {
    sections.push(type_params);
  }

  if let Some(index_signatures) = render_index_signatures(
    ctx,
    &interface_def.index_signatures,
    interface_diff.and_then(|d| d.index_signature_changes.as_ref()),
  ) {
    sections.push(index_signatures);
  }

  if let Some(construct_signatures) = render_construct_signatures(
    ctx,
    &interface_def.constructors,
    interface_diff.and_then(|d| d.constructor_changes.as_ref()),
  ) {
    sections.push(construct_signatures);
  }

  if let Some(call_signatures) = render_call_signatures(
    ctx,
    &interface_def.call_signatures,
    interface_diff.and_then(|d| d.call_signature_changes.as_ref()),
  ) {
    sections.push(call_signatures);
  }

  if let Some(properties) = render_properties(
    ctx,
    name,
    &interface_def.properties,
    interface_diff.and_then(|d| d.property_changes.as_ref()),
  ) {
    sections.push(properties);
  }

  if let Some(methods) = render_methods(
    ctx,
    name,
    &interface_def.methods,
    interface_diff.and_then(|d| d.method_changes.as_ref()),
  ) {
    sections.push(methods);
  }

  sections
}

pub(crate) fn render_index_signatures(
  ctx: &RenderContext,
  index_signatures: &[crate::ts_type::IndexSignatureDef],
  index_signatures_diff: Option<&crate::diff::InterfaceIndexSignaturesDiff>,
) -> Option<SectionCtx> {
  let empty_sigs = Vec::new();
  let empty_mod = Vec::new();
  let total = index_signatures.len();

  super::render_index_signatures_with_diff(
    ctx,
    index_signatures,
    index_signatures_diff.map_or(&empty_sigs, |d| &d.removed),
    index_signatures_diff.map_or(&empty_mod, |d| &d.modified),
    |i, _sig| {
      let diff = index_signatures_diff?;
      if !diff.added.is_empty() && i >= total - diff.added.len() {
        Some(DiffStatus::Added)
      } else if diff.modified.iter().any(|m| m.index == i) {
        Some(DiffStatus::Modified)
      } else {
        None
      }
    },
  )
}

pub(crate) fn render_construct_signatures(
  ctx: &RenderContext,
  constructors: &[crate::ts_type::ConstructorDef],
  constructor_changes: Option<&crate::diff::InterfaceConstructorsDiff>,
) -> Option<SectionCtx> {
  if constructors.is_empty()
    && constructor_changes.is_none_or(|d| d.removed.is_empty())
  {
    return None;
  }

  let mut items = constructors
    .iter()
    .enumerate()
    .map(|(i, constructor)| {
      let id = IdBuilder::new(ctx)
        .kind(IdKind::ConstructSignature)
        .index(i)
        .build();

      let return_type = constructor
        .return_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      let tags = Tag::from_js_doc(&constructor.js_doc);

      let ctor_diff = constructor_changes.and_then(|d| {
        d.modified
          .iter()
          .find(|m| m.param_count == constructor.params.len())
      });

      let diff_status = if let Some(diff) = constructor_changes {
        if diff.added.iter().any(|a| a == constructor) {
          Some(DiffStatus::Added)
        } else if ctor_diff.is_some() {
          Some(DiffStatus::Modified)
        } else {
          None
        }
      } else {
        None
      };

      let old_tags = if matches!(
        diff_status,
        Some(DiffStatus::Modified | DiffStatus::Renamed { .. })
      ) {
        Some(super::compute_old_tags(&tags, None, None, None, None))
      } else {
        None
      };

      let old_content = if matches!(diff_status, Some(DiffStatus::Modified)) {
        ctor_diff.and_then(|cd| {
          super::function::render_old_function_summary(
            ctx,
            &constructor.type_params,
            &constructor.params,
            &constructor.return_type,
            cd.type_params_change.as_ref(),
            cd.params_change.as_ref(),
            cd.return_type_change.as_ref(),
          )
        })
      } else {
        None
      };

      let mut entry = DocEntryCtx::new(
        ctx,
        id,
        None,
        None,
        &format!(
          "{}({}){return_type}",
          type_params_summary(ctx, &constructor.type_params),
          render_params(ctx, &constructor.params),
        ),
        tags,
        constructor.js_doc.doc.as_deref(),
        &constructor.location,
        diff_status,
        old_content,
        old_tags,
        ctor_diff.and_then(|cd| cd.js_doc_change.as_ref()),
      );
      entry.name_prefix = Some("new".into());

      entry
    })
    .collect::<Vec<DocEntryCtx>>();

  if let Some(diff) = constructor_changes {
    for constructor in &diff.removed {
      let id = IdBuilder::new(ctx)
        .kind(IdKind::ConstructSignature)
        .index(items.len())
        .build();

      let tags = Tag::from_js_doc(&constructor.js_doc);

      let return_type = constructor
        .return_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      let mut entry = DocEntryCtx::removed(
        ctx,
        id,
        None,
        None,
        &format!(
          "{}({}){return_type}",
          type_params_summary(ctx, &constructor.type_params),
          render_params(ctx, &constructor.params),
        ),
        tags,
        constructor.js_doc.doc.as_deref(),
        &constructor.location,
      );
      entry.name_prefix = Some("new".into());

      items.push(entry);
    }
  }

  if items.is_empty() {
    return None;
  }

  Some(SectionCtx::new(
    ctx,
    "Constructors",
    SectionContentCtx::DocEntry(items),
  ))
}

pub(crate) fn render_call_signatures(
  ctx: &RenderContext,
  call_signatures: &[crate::ts_type::CallSignatureDef],
  call_signatures_diff: Option<&crate::diff::CallSignaturesDiff>,
) -> Option<SectionCtx> {
  if call_signatures.is_empty()
    && call_signatures_diff.is_none_or(|d| d.removed.is_empty())
  {
    return None;
  }

  let mut items = call_signatures
    .iter()
    .enumerate()
    .map(|(i, call_signature)| {
      let id = IdBuilder::new(ctx)
        .kind(IdKind::CallSignature)
        .index(i)
        .build();

      let ts_type = call_signature
        .ts_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      let tags = Tag::from_js_doc(&call_signature.js_doc);

      let sig_diff = call_signatures_diff
        .and_then(|d| d.modified.iter().find(|m| m.index == i));

      let diff_status = if let Some(diff) = call_signatures_diff {
        if diff.added.iter().any(|a| a == call_signature) {
          Some(DiffStatus::Added)
        } else if sig_diff.is_some() {
          Some(DiffStatus::Modified)
        } else {
          None
        }
      } else {
        None
      };

      let old_tags = if matches!(
        diff_status,
        Some(DiffStatus::Modified | DiffStatus::Renamed { .. })
      ) {
        Some(super::compute_old_tags(&tags, None, None, None, None))
      } else {
        None
      };

      let old_content = if matches!(diff_status, Some(DiffStatus::Modified)) {
        sig_diff.and_then(|sd| {
          super::function::render_old_function_summary(
            ctx,
            &call_signature.type_params,
            &call_signature.params,
            &call_signature.ts_type,
            sd.type_params_change.as_ref(),
            sd.params_change.as_ref(),
            sd.ts_type_change.as_ref(),
          )
        })
      } else {
        None
      };

      DocEntryCtx::new(
        ctx,
        id,
        None,
        None,
        &format!(
          "{}({}){ts_type}",
          type_params_summary(ctx, &call_signature.type_params),
          render_params(ctx, &call_signature.params),
        ),
        tags,
        call_signature.js_doc.doc.as_deref(),
        &call_signature.location,
        diff_status,
        old_content,
        old_tags,
        sig_diff.and_then(|sd| sd.js_doc_change.as_ref()),
      )
    })
    .collect::<Vec<DocEntryCtx>>();

  if let Some(diff) = call_signatures_diff {
    for call_signature in &diff.removed {
      let id = IdBuilder::new(ctx)
        .kind(IdKind::CallSignature)
        .index(items.len())
        .build();

      let tags = Tag::from_js_doc(&call_signature.js_doc);

      let ts_type = call_signature
        .ts_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      items.push(DocEntryCtx::removed(
        ctx,
        id,
        None,
        None,
        &format!(
          "{}({}){ts_type}",
          type_params_summary(ctx, &call_signature.type_params),
          render_params(ctx, &call_signature.params),
        ),
        tags,
        call_signature.js_doc.doc.as_deref(),
        &call_signature.location,
      ));
    }
  }

  if items.is_empty() {
    return None;
  }

  Some(SectionCtx::new(
    ctx,
    "Call Signatures",
    SectionContentCtx::DocEntry(items),
  ))
}

pub(crate) fn render_properties(
  ctx: &RenderContext,
  interface_name: &str,
  properties: &[crate::ts_type::PropertyDef],
  properties_diff: Option<&crate::diff::InterfacePropertiesDiff>,
) -> Option<SectionCtx> {
  if properties.is_empty()
    && properties_diff.is_none_or(|d| d.removed.is_empty())
  {
    return None;
  }

  let mut items = properties
    .iter()
    .map(|property| {
      let id = IdBuilder::new(ctx)
        .kind(IdKind::Property)
        .name(&property.name)
        .build();
      let default_value = property
        .js_doc
        .tags
        .iter()
        .find_map(|tag| {
          if let crate::js_doc::JsDocTag::Default { value, .. } = tag {
            Some(format!(
              r#"<span><span class="font-normal"> = </span>{}</span>"#,
              html_escape::encode_text(value)
            ))
          } else {
            None
          }
        })
        .unwrap_or_default();

      let ts_type = property
        .ts_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      let mut tags = Tag::from_js_doc(&property.js_doc);
      if property.readonly {
        tags.insert(Tag::Readonly);
      }
      if property.optional {
        tags.insert(Tag::Optional);
      }

      let diff_status = if let Some(diff) = properties_diff {
        if diff.added.iter().any(|p| p.name == property.name) {
          Some(DiffStatus::Added)
        } else if let Some(md) =
          diff.modified.iter().find(|p| p.name == property.name)
        {
          if let Some(name_change) = &md.name_change {
            Some(DiffStatus::Renamed {
              old_name: name_change.old.clone(),
            })
          } else {
            Some(DiffStatus::Modified)
          }
        } else {
          None
        }
      } else {
        None
      };

      let (old_content, old_tags, prop_diff) = if matches!(
        diff_status,
        Some(DiffStatus::Modified | DiffStatus::Renamed { .. })
      ) {
        let prop_diff = properties_diff
          .and_then(|pc| pc.modified.iter().find(|p| p.name == property.name));

        let old_content = prop_diff
          .and_then(|pd| pd.type_change.as_ref())
          .map(|tc| render_type_def_colon(ctx, &tc.old));

        let old_tags = prop_diff.map(|diff| {
          super::compute_old_tags(
            &tags,
            None,
            diff.readonly_change.as_ref(),
            None,
            diff.optional_change.as_ref(),
          )
        });

        (old_content, old_tags, prop_diff)
      } else {
        (None, None, None)
      };

      DocEntryCtx::new(
        ctx,
        id,
        Some(if property.computed {
          format!("[{}]", html_escape::encode_text(&property.name))
        } else {
          html_escape::encode_text(&property.name).into_owned()
        }),
        ctx.lookup_symbol_href(&qualify_drilldown_name(
          interface_name,
          &property.name,
          true,
        )),
        &format!("{ts_type}{default_value}"),
        tags,
        property.js_doc.doc.as_deref(),
        &property.location,
        diff_status,
        old_content,
        old_tags,
        prop_diff.and_then(|pd| pd.js_doc_change.as_ref()),
      )
    })
    .collect::<Vec<DocEntryCtx>>();

  if let Some(prop_diff) = properties_diff {
    for removed_prop in &prop_diff.removed {
      super::push_removed_property_entry(
        ctx,
        &removed_prop.name,
        removed_prop.ts_type.as_ref(),
        &removed_prop.location,
        &mut items,
      );
    }
  }

  if items.is_empty() {
    None
  } else {
    Some(SectionCtx::new(
      ctx,
      "Properties",
      SectionContentCtx::DocEntry(items),
    ))
  }
}

pub(crate) fn render_methods(
  ctx: &RenderContext,
  interface_name: &str,
  methods: &[crate::ts_type::MethodDef],
  methods_diff: Option<&crate::diff::InterfaceMethodsDiff>,
) -> Option<SectionCtx> {
  if methods.is_empty() && methods_diff.is_none_or(|d| d.removed.is_empty()) {
    return None;
  }

  let mut items = methods
    .iter()
    .enumerate()
    .map(|(i, method)| {
      let id = IdBuilder::new(ctx)
        .kind(IdKind::Method)
        .name(&method.name)
        .index(i)
        .build();

      let name = if method.name == "new" {
        "<span>new</span>".to_string()
      } else if method.computed {
        format!("[{}]", html_escape::encode_text(&method.name))
      } else {
        html_escape::encode_text(&method.name).into_owned()
      };

      let return_type = method
        .return_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      let mut tags = Tag::from_js_doc(&method.js_doc);
      if method.optional {
        tags.insert(Tag::Optional);
      }

      let diff_status = if let Some(diff) = methods_diff {
        if diff.added.iter().any(|m| m.name == method.name) {
          Some(DiffStatus::Added)
        } else if let Some(md) =
          diff.modified.iter().find(|m| m.name == method.name)
        {
          if let Some(name_change) = &md.name_change {
            Some(DiffStatus::Renamed {
              old_name: name_change.old.clone(),
            })
          } else {
            Some(DiffStatus::Modified)
          }
        } else {
          None
        }
      } else {
        None
      };

      let (old_content, old_tags, method_diff) = if matches!(
        diff_status,
        Some(DiffStatus::Modified | DiffStatus::Renamed { .. })
      ) {
        let method_diff = methods_diff
          .and_then(|mc| mc.modified.iter().find(|m| m.name == *method.name));

        let old_content = method_diff.and_then(|md| {
          super::function::render_old_function_summary(
            ctx,
            &method.type_params,
            &method.params,
            &method.return_type,
            md.type_params_change.as_ref(),
            md.params_change.as_ref(),
            md.return_type_change.as_ref(),
          )
        });

        let old_tags = method_diff.map(|diff| {
          super::compute_old_tags(
            &tags,
            None,
            None,
            None,
            diff.optional_change.as_ref(),
          )
        });

        (old_content, old_tags, method_diff)
      } else {
        (None, None, None)
      };

      DocEntryCtx::new(
        ctx,
        id,
        Some(name),
        ctx.lookup_symbol_href(&qualify_drilldown_name(
          interface_name,
          &method.name,
          true,
        )),
        &format!(
          "{}({}){return_type}",
          type_params_summary(ctx, &method.type_params),
          render_params(ctx, &method.params)
        ),
        tags,
        method.js_doc.doc.as_deref(),
        &method.location,
        diff_status,
        old_content,
        old_tags,
        method_diff.and_then(|md| md.js_doc_change.as_ref()),
      )
    })
    .collect::<Vec<DocEntryCtx>>();

  if let Some(method_diff) = methods_diff {
    for removed_method in &method_diff.removed {
      let return_type = removed_method
        .return_type
        .as_ref()
        .map(|ts_type| render_type_def_colon(ctx, ts_type))
        .unwrap_or_default();

      super::push_removed_method_entry(
        ctx,
        &removed_method.name,
        &format!(
          "{}({}){return_type}",
          type_params_summary(ctx, &removed_method.type_params),
          render_params(ctx, &removed_method.params)
        ),
        &removed_method.location,
        &mut items,
      );
    }
  }

  if items.is_empty() {
    None
  } else {
    Some(SectionCtx::new(
      ctx,
      "Methods",
      SectionContentCtx::DocEntry(items),
    ))
  }
}
