import os from "node:os";
import path from "node:path";

import { extractZillowIdentifierHints } from "../../web-automation/scripts/zillow-identifiers.js";
import { discoverListingSources, type ListingDiscoveryResult } from "./listing-discovery.js";
import { extractPhotoData, type PhotoExtractionResult, type PhotoSource } from "./photo-review.js";
import { resolvePublicRecords, type PublicRecordsResolution } from "./public-records.js";
import {
  isDecisionGradeReportPayload,
  renderReportPdf,
  type ReportPayload
} from "./report-pdf.js";

export interface AssessPropertyOptions {
  address: string;
  assessmentPurpose?: string;
  recipientEmails?: string[] | string;
  output?: string;
  parcelId?: string;
  listingGeoId?: string;
  listingSourceUrl?: string;
}

export interface AssessPropertyResult {
  ok: true;
  needsAssessmentPurpose: boolean;
  needsRecipientEmails: boolean;
  pdfReady: boolean;
  message: string;
  outputPath: string | null;
  reportPayload: ReportPayload | null;
  publicRecords: PublicRecordsResolution | null;
}

interface AssessPropertyDeps {
  resolvePublicRecordsFn?: typeof resolvePublicRecords;
  renderReportPdfFn?: typeof renderReportPdf;
  discoverListingSourcesFn?: typeof discoverListingSources;
  extractPhotoDataFn?: typeof extractPhotoData;
  extractZillowIdentifierHintsFn?: typeof extractZillowIdentifierHints;
}

interface PurposeGuidance {
  label: string;
  snapshot: string;
  like: string;
  caution: string;
  comp: string;
  carry: string;
  diligence: string;
  verdict: string;
}

function asStringArray(value: unknown): string[] {
  if (value == null) return [];
  if (typeof value === "string") {
    return value
      .split(",")
      .map((item) => item.trim())
      .filter(Boolean);
  }
  if (Array.isArray(value)) {
    return value.flatMap((item) => asStringArray(item));
  }
  return [String(value).trim()].filter(Boolean);
}

function shouldRenderPdf(
  options: AssessPropertyOptions,
  recipientEmails: string[]
): boolean {
  return Boolean(options.output || recipientEmails.length);
}

function slugify(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80) || "property";
}

function pushLink(
  target: Array<{ label: string; url: string }>,
  label: string,
  url: unknown
): void {
  if (typeof url !== "string" || !url.trim()) return;
  const normalized = url.trim();
  if (!target.some((item) => item.url === normalized)) {
    target.push({ label, url: normalized });
  }
}

function buildPublicRecordLinks(
  publicRecords: PublicRecordsResolution
): Array<{ label: string; url: string }> {
  const links: Array<{ label: string; url: string }> = [];
  pushLink(links, "Census Geocoder", publicRecords.officialLinks.censusGeocoder);
  pushLink(
    links,
    "Texas Comptroller County Directory",
    publicRecords.officialLinks.texasCountyDirectory
  );
  pushLink(
    links,
    "Texas Property Tax Portal",
    publicRecords.officialLinks.texasPropertyTaxPortal
  );
  pushLink(links, "Appraisal District Website", publicRecords.appraisalDistrict?.Website);
  pushLink(
    links,
    "Appraisal District Directory Page",
    publicRecords.appraisalDistrict?.directoryPage
  );
  pushLink(
    links,
    "Tax Assessor / Collector Website",
    publicRecords.taxAssessorCollector?.Website
  );
  pushLink(
    links,
    "Tax Assessor / Collector Directory Page",
    publicRecords.taxAssessorCollector?.directoryPage
  );
  pushLink(links, "CAD Property Detail", publicRecords.propertyDetails?.sourceUrl);
  return links;
}

function normalizePurpose(value: string): string {
  return value.trim().replace(/\s+/g, " ");
}

function getPurposeGuidance(purpose: string): PurposeGuidance {
  const normalized = purpose.toLowerCase();

  if (/(daughter|son|college|student|school|campus)/i.test(normalized)) {
    return {
      label: purpose,
      snapshot: "Purpose fit: evaluate this as practical housing for a student, with safety, commute, durability, and oversight burden in mind.",
      like: "This purpose is best served by simple logistics, durable finishes, and a layout that is easy to live in and maintain.",
      caution: "Student-use housing can become a bad fit when commute friction, fragile finishes, or management burden are underestimated.",
      comp: "Comp work should emphasize campus proximity, day-to-day practicality, and whether comparable student-friendly options exist at lower all-in cost.",
      carry: "Carry view should account for parent-risk factors, furnishing/setup costs, upkeep burden, and whether renting may still be the lower-risk option.",
      diligence: "Confirm commute, safety, parking, roommate practicality if relevant, and whether the property is easy to maintain from a distance.",
      verdict: `Assessment purpose: ${purpose}. The final recommendation should prioritize practicality, safety, and management burden over generic resale talking points.`
    };
  }

  if (/(vacation|second home|weekend|personal use|beach|getaway)/i.test(normalized)) {
    return {
      label: purpose,
      snapshot: "Purpose fit: evaluate this as a vacation home with personal-use fit and carrying-cost tolerance in mind.",
      like: "A vacation-home decision can justify paying for lifestyle fit, but only if ongoing friction is acceptable.",
      caution: "Vacation home ownership can hide real recurring cost drag when insurance, HOA, storm exposure, and deferred maintenance are under-modeled.",
      comp: "Comp work should focus on lifestyle alternatives, micro-location quality, and whether the premium over substitutes is defensible for a vacation home.",
      carry: "Carry view should stress-test second-home costs, especially insurance, HOA, special assessments, and low-utilization months.",
      diligence: "Confirm rules, reserves, storm or flood exposure, and whether the property still makes sense if usage ends up lower than expected.",
      verdict: `Assessment purpose: ${purpose}. The final call should weigh personal-use fit against ongoing friction, not just headline list price.`
    };
  }

  if (/(invest|rental|cash[\s-]?flow|income|flip|str|airbnb)/i.test(normalized)) {
    return {
      label: purpose,
      snapshot: `Purpose fit: evaluate this as an income property first, not a generic owner-occupant home.`,
      like: "Purpose-aligned upside will depend on durable rent support, manageable make-ready, and exit liquidity.",
      caution: "An investment property is not attractive just because the address clears basic public-record checks; rent support and margin still decide the deal.",
      comp: "Comp work should focus on resale liquidity and true rent support for an income property, not only nearby asking prices.",
      carry: "Carry view should underwrite this as an income property with taxes, insurance, HOA, maintenance, vacancy, and management friction.",
      diligence: "Confirm realistic rent, reserve for turns and repairs, and whether the building/submarket has enough liquidity for an investment property exit.",
      verdict: `Assessment purpose: ${purpose}. Do not issue a buy/pass conclusion until the property clears rent support, carry, and make-ready standards for an investment property.`
    };
  }

  return {
    label: purpose,
    snapshot: `Purpose fit: ${purpose}. The final recommendation should be explicitly tested against that goal.`,
    like: "The assessment should stay anchored to the stated purpose rather than defaulting to generic market commentary.",
    caution: "Even a clean public-record and photo intake is not enough if the property does not fit the stated purpose.",
    comp: "Comp work should compare against alternatives that solve the same purpose, not just nearby listings.",
    carry: "Carry view should reflect the stated purpose and the real friction it implies.",
    diligence: "Purpose-specific diligence should be listed explicitly before a final buy/pass/offer recommendation.",
    verdict: `Assessment purpose: ${purpose}. The final conclusion must be explained in terms of that stated objective.`
  };
}

function inferSourceFromUrl(rawUrl: string): PhotoSource | null {
  try {
    const url = new URL(rawUrl);
    const host = url.hostname.toLowerCase();
    if (host.includes("zillow.com")) return "zillow";
    if (host.includes("har.com")) return "har";
    return null;
  } catch {
    return null;
  }
}

interface ResolvedListingCandidates {
  attempts: string[];
  listingUrls: Array<{ label: string; url: string }>;
  zillowUrl: string | null;
  harUrl: string | null;
}

async function resolveListingCandidates(
  options: AssessPropertyOptions,
  discoverListingSourcesFn: typeof discoverListingSources
): Promise<ResolvedListingCandidates> {
  const attempts: string[] = [];
  const listingUrls: Array<{ label: string; url: string }> = [];

  const addListingUrl = (label: string, url: string | null | undefined): void => {
    if (!url) return;
    if (!listingUrls.some((item) => item.url === url)) {
      listingUrls.push({ label, url });
    }
  };

  let zillowUrl: string | null = null;
  let harUrl: string | null = null;

  if (options.listingSourceUrl) {
    const explicitSource = inferSourceFromUrl(options.listingSourceUrl);
    addListingUrl("Explicit Listing Source", options.listingSourceUrl);
    if (explicitSource === "zillow") {
      zillowUrl = options.listingSourceUrl;
      attempts.push(`Using explicit Zillow listing URL: ${options.listingSourceUrl}`);
    } else if (explicitSource === "har") {
      harUrl = options.listingSourceUrl;
      attempts.push(`Using explicit HAR listing URL: ${options.listingSourceUrl}`);
    } else {
      attempts.push(
        `Explicit listing URL was provided but is not a supported Zillow/HAR photo source: ${options.listingSourceUrl}`
      );
    }
  }

  if (!zillowUrl && !harUrl) {
    const discovered = await discoverListingSourcesFn(options.address);
    attempts.push(...discovered.attempts);
    zillowUrl = discovered.zillowUrl;
    harUrl = discovered.harUrl;
    addListingUrl("Discovered Zillow Listing", zillowUrl);
    addListingUrl("Discovered HAR Listing", harUrl);
  }

  return {
    attempts,
    listingUrls,
    zillowUrl,
    harUrl,
  };
}

async function resolvePhotoReview(
  listingCandidates: ResolvedListingCandidates,
  extractPhotoDataFn: typeof extractPhotoData,
  additionalAttempts: string[] = []
): Promise<{
  listingUrls: Array<{ label: string; url: string }>;
  photoReview: Record<string, unknown>;
}> {
  const attempts: string[] = [...listingCandidates.attempts, ...additionalAttempts];
  const listingUrls = [...listingCandidates.listingUrls];
  const candidates: Array<{ source: PhotoSource; url: string }> = [];
  if (listingCandidates.zillowUrl) candidates.push({ source: "zillow", url: listingCandidates.zillowUrl });
  if (listingCandidates.harUrl) candidates.push({ source: "har", url: listingCandidates.harUrl });

  let extracted: PhotoExtractionResult | null = null;
  for (const candidate of candidates) {
    try {
      const result = await extractPhotoDataFn(candidate.source, candidate.url);
      extracted = result;
      attempts.push(
        `${candidate.source} photo extraction succeeded with ${result.photoCount} photos.`
      );
      if (result.notes.length) {
        attempts.push(...result.notes);
      }
      break;
    } catch (error) {
      attempts.push(
        `${candidate.source} photo extraction failed: ${error instanceof Error ? error.message : String(error)}`
      );
    }
  }

  if (extracted) {
    return {
      listingUrls,
      photoReview: {
        status: "completed",
        source: extracted.source,
        photoCount: extracted.photoCount,
        expectedPhotoCount: extracted.expectedPhotoCount ?? null,
        imageUrls: extracted.imageUrls,
        attempts,
        summary:
          "Photo URLs were collected successfully. A decision-grade condition read still requires reviewing the extracted image set.",
      }
    };
  }

  if (!candidates.length) {
    attempts.push(
      "No supported Zillow or HAR listing URL was available for photo extraction."
    );
  }

  return {
    listingUrls,
    photoReview: {
      status: "not completed",
      source: candidates.length ? "listing source attempted" : "no supported listing source",
      attempts,
      summary:
        "Condition review is incomplete until Zillow or HAR photos are extracted and inspected."
    }
  };
}

export function buildAssessmentReportPayload(
  options: AssessPropertyOptions,
  publicRecords: PublicRecordsResolution,
  listingUrls: Array<{ label: string; url: string }>,
  photoReview: Record<string, unknown>
): ReportPayload {
  const recipientEmails = asStringArray(options.recipientEmails);
  const matchedAddress = publicRecords.matchedAddress || options.address;
  const publicRecordLinks = buildPublicRecordLinks(publicRecords);
  const sourceLinks = [...publicRecordLinks];
  const purpose = normalizePurpose(options.assessmentPurpose || "");
  const purposeGuidance = getPurposeGuidance(purpose);

  for (const item of listingUrls) {
    pushLink(sourceLinks, item.label, item.url);
  }

  const jurisdiction =
    publicRecords.county.name && publicRecords.appraisalDistrict
      ? `${publicRecords.county.name} Appraisal District`
      : publicRecords.county.name || publicRecords.state.name || "Official public-record jurisdiction";
  const assessedTotalValue = publicRecords.propertyDetails?.assessedTotalValue ?? null;
  const ownerName = publicRecords.propertyDetails?.ownerName ?? undefined;
  const landValue = publicRecords.propertyDetails?.landValue ?? undefined;
  const improvementValue = publicRecords.propertyDetails?.improvementValue ?? undefined;
  const exemptions = publicRecords.propertyDetails?.exemptions?.length
    ? publicRecords.propertyDetails.exemptions
    : undefined;

  return {
    recipientEmails,
    assessmentPurpose: purposeGuidance.label,
    reportTitle: "Property Assessment Report",
    subtitle: "Address-first intake with public-record enrichment and approval-safe photo-source orchestration.",
    subjectProperty: {
      address: matchedAddress,
      county: publicRecords.county.name || "N/A",
      state: publicRecords.state.code || publicRecords.state.name || "N/A",
      geoid: publicRecords.geoid || "N/A"
    },
    verdict: {
      decision: "pending",
      fairValueRange: "Not established",
      offerGuidance: purposeGuidance.verdict
    },
    snapshot: [
      `Matched address: ${matchedAddress}`,
      `Jurisdiction anchor: ${publicRecords.county.name || "Unknown county"}, ${publicRecords.state.code || publicRecords.state.name || "Unknown state"}.`,
      publicRecords.geoid ? `Census block GEOID: ${publicRecords.geoid}.` : "Census block GEOID not returned.",
      assessedTotalValue != null
        ? `Official CAD assessed value: $${assessedTotalValue.toLocaleString("en-US")}.`
        : "Official CAD assessed value was not retrieved.",
      purposeGuidance.snapshot
    ],
    whatILike: [
      "Address-first flow avoids treating listing-site geo IDs as assessor record keys.",
      publicRecords.appraisalDistrict
        ? "Official appraisal-district contact and website were identified from public records."
        : "Official public-record geography was identified.",
      purposeGuidance.like
    ],
    whatIDontLike: [
      "This assess helper still needs listing facts, comp analysis, and a human/model review of the extracted photo set before any final valuation claim.",
      purposeGuidance.caution
    ],
    compView: [purposeGuidance.comp],
    carryView: [purposeGuidance.carry],
    risksAndDiligence: [
      ...publicRecords.lookupRecommendations,
      ...(publicRecords.propertyDetails?.notes || []),
      purposeGuidance.diligence
    ],
    photoReview,
    publicRecords: {
      jurisdiction,
      accountNumber:
        publicRecords.propertyDetails?.propertyId ||
        options.parcelId ||
        publicRecords.sourceIdentifierHints.parcelId,
      ownerName,
      landValue,
      improvementValue,
      assessedTotalValue,
      exemptions,
      links: publicRecordLinks
    },
    sourceLinks
  };
}

export async function assessProperty(
  options: AssessPropertyOptions,
  deps: AssessPropertyDeps = {}
): Promise<AssessPropertyResult> {
  const purpose = normalizePurpose(options.assessmentPurpose || "");
  if (!purpose) {
    return {
      ok: true,
      needsAssessmentPurpose: true,
      needsRecipientEmails: false,
      pdfReady: false,
      message:
        "Missing assessment purpose. Stop and ask the user why they want this property assessed before producing a decision-grade analysis.",
      outputPath: null,
      reportPayload: null,
      publicRecords: null
    };
  }

  const resolvePublicRecordsFn = deps.resolvePublicRecordsFn || resolvePublicRecords;
  const renderReportPdfFn = deps.renderReportPdfFn || renderReportPdf;
  const discoverListingSourcesFn = deps.discoverListingSourcesFn || discoverListingSources;
  const extractPhotoDataFn = deps.extractPhotoDataFn || extractPhotoData;
  const extractZillowIdentifierHintsFn =
    deps.extractZillowIdentifierHintsFn || extractZillowIdentifierHints;

  const listingCandidates = await resolveListingCandidates(
    { ...options, assessmentPurpose: purpose },
    discoverListingSourcesFn
  );

  const identifierAttempts: string[] = [];
  let effectiveParcelId = options.parcelId;
  if (!effectiveParcelId && listingCandidates.zillowUrl) {
    try {
      const hints = await extractZillowIdentifierHintsFn(listingCandidates.zillowUrl);
      effectiveParcelId = hints.parcelId || hints.apn || effectiveParcelId;
      if (Array.isArray(hints.notes) && hints.notes.length) {
        identifierAttempts.push(...hints.notes);
      }
    } catch (error) {
      identifierAttempts.push(
        `Zillow parcel/APN extraction failed: ${error instanceof Error ? error.message : String(error)}`
      );
    }
  }

  const effectiveListingSourceUrl =
    options.listingSourceUrl || listingCandidates.zillowUrl || listingCandidates.harUrl || undefined;

  const publicRecords = await resolvePublicRecordsFn(options.address, {
    parcelId: effectiveParcelId,
    listingGeoId: options.listingGeoId,
    listingSourceUrl: effectiveListingSourceUrl
  });

  const photoResolution = await resolvePhotoReview(
    listingCandidates,
    extractPhotoDataFn,
    identifierAttempts
  );

  const reportPayload = buildAssessmentReportPayload(
    {
      ...options,
      assessmentPurpose: purpose,
      parcelId: effectiveParcelId,
      listingSourceUrl: effectiveListingSourceUrl
    },
    publicRecords,
    photoResolution.listingUrls,
    photoResolution.photoReview
  );
  const recipientEmails = asStringArray(options.recipientEmails);
  const renderPdf = shouldRenderPdf(options, recipientEmails);

  if (renderPdf && !recipientEmails.length) {
    return {
      ok: true,
      needsAssessmentPurpose: false,
      needsRecipientEmails: true,
      pdfReady: false,
      message:
        "Missing target email. Stop and ask the user for target email address(es) before generating or sending the property assessment PDF.",
      outputPath: null,
      reportPayload,
      publicRecords
    };
  }

  if (!renderPdf) {
    return {
      ok: true,
      needsAssessmentPurpose: false,
      needsRecipientEmails: false,
      pdfReady: true,
      message:
        "Assessment payload is ready to render later. Review the analysis now; recipient email is only needed when you want the PDF.",
      outputPath: null,
      reportPayload,
      publicRecords
    };
  }

  if (!isDecisionGradeReportPayload(reportPayload)) {
    return {
      ok: true,
      needsAssessmentPurpose: false,
      needsRecipientEmails: false,
      pdfReady: false,
      message:
        "The report payload is still preliminary. Do not render or send the PDF until comps, valuation, and a decision-grade verdict are completed.",
      outputPath: null,
      reportPayload,
      publicRecords
    };
  }

  const outputPath =
    options.output ||
    path.join(
      os.tmpdir(),
      `property-assessment-${slugify(publicRecords.matchedAddress || options.address)}-${Date.now()}.pdf`
    );

  const renderedPath = await renderReportPdfFn(reportPayload, outputPath);
  return {
    ok: true,
    needsAssessmentPurpose: false,
    needsRecipientEmails: false,
    pdfReady: true,
    message: `Property assessment PDF rendered: ${renderedPath}`,
    outputPath: renderedPath,
    reportPayload,
    publicRecords
  };
}
