#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pyright: reportUnknownArgumentType=false
# pyright: reportUnknownMemberType=false
# pyright: reportUnknownVariableType=false
"""Convert UCLA Library CSV files for Ursus, our Blacklight installation."""

import asyncio
import csv
import importlib.metadata
import logging
import sys
from datetime import datetime, timezone
from getpass import getuser
from typing import (
    Any,
    Literal,
    TextIO,
)

import click
import httpx
import pydantic
import requests
import rich.progress
from pysolr import Solr, SolrError  # type: ignore
from rich.console import Console
from rich.table import Table

from feed_ursus.solr_record import IngestSolrRecord, UrsusSolrRecord
from feed_ursus.util import Ark, UrsusId, parse_list


class Importer:
    solr_url: str
    solr_client: Solr
    async_client: httpx.AsyncClient

    ingest_id: str  # for sync load_csv
    collection_names: dict[Ark, str]

    connection_pool = asyncio.Semaphore(3)

    def __init__(self, solr_url: str):
        self.solr_url = solr_url
        self.solr_client = Solr(solr_url, always_commit=True)
        self.async_client = httpx.AsyncClient()

        self.ingest_id = f"{datetime.now(timezone.utc).isoformat()}-{getuser()}"
        self.collection_names = {}

        self.collection_names_from_solr()

    def get_ingest_record(self, filenames: list[str]) -> IngestSolrRecord:
        return IngestSolrRecord(
            id=self.ingest_id,
            is_ingest_bsi=True,
            ingest_filenames_ssim=filenames,
            feed_ursus_version_ssi=importlib.metadata.version("feed_ursus"),
            ingest_user_ssi=getuser(),
            csv_files_tsm=[
                open(filename, encoding="utf-8").read()
                for filename in rich.progress.track(
                    filenames, description=f"loading {len(filenames)} files..."
                )
            ],
        )

    def load_csv(self, filenames: list[str], batch: bool):
        """Load data from a csv.

        Args:
            filenames: A list of CSV filenames.
        """

        csv_data = {
            row["Item ARK"]: row
            for filename in rich.progress.track(
                filenames, description=f"loading {len(filenames)} files..."
            )
            for row in csv.DictReader(open(filename, encoding="utf-8"))
        }

        self.ingest_id = f"{datetime.now(timezone.utc).isoformat()}-{getuser()}"
        self.collection_names.update(
            {
                row["Item ARK"]: row["Title"]
                for row in csv_data.values()
                if row.get("Object Type") == "Collection"
            }
        )

        mapped_records: list[IngestSolrRecord | UrsusSolrRecord] = [
            self.get_ingest_record(filenames)
        ]
        for row in rich.progress.track(
            csv_data.values(), description=f"Importing {len(csv_data)} records..."
        ):
            try:
                if row.get("Object Type") not in ("ChildWork", "Page"):
                    mapped_records.append(self.map_record(row))
            except pydantic.ValidationError as e:
                row_handle = row.get("Item ARK") or row.get("Item Title") or row

                # Note: using "\r" overwrites what would otherwise be a duplicated progress bar
                rich.print(f"\rCould not import row {row_handle}:")
                rich.print(e)
                rich.print("\n")

        if batch:
            print("Submitting records in batch mode...")
            try:
                self.solr_client.add(  # pyright: ignore[reportUnknownMemberType]
                    [record.model_dump(mode="json") for record in mapped_records]
                )
            except SolrError as e:
                print(f"Error adding records in batch mode: {e}")

        else:
            print("Submitting records one by one...")
            for mapped_record in mapped_records:
                try:
                    self.solr_client.add(mapped_record.model_dump(mode="json"))  # pyright: ignore[reportUnknownMemberType]

                except SolrError as e:
                    print(f"Error adding record {mapped_record.solr_id}: {e}")

    def delete(self, items: list[str], yes: bool):
        """Delete records from a Solr index.

        Args:
            solr_url: URL of a solr instance.
            items: List of items to delete. Can be ARKs, Solr IDs, or csv filenames.
                If a csv filename is provided, all ARKs in the file will be deleted.
        """

        delete_ids: list[str] = []
        for item in items:
            if item.endswith(".csv"):
                with open(item, "r", encoding="utf-8") as stream:
                    csv_data = csv.DictReader(stream)
                    delete_ids.extend(
                        pydantic.TypeAdapter(UrsusId).validate_python(row["Item ARK"])
                        for row in csv_data
                    )
            elif item.startswith("ark:/"):
                delete_ids.append(pydantic.TypeAdapter(UrsusId).validate_python(item))
            else:
                delete_ids.append(item)

        delete_work_ids: list[UrsusId] = []
        delete_collections: list[UrsusId] = []
        for record in (
            requests.get(
                f"{self.solr_client.url}/get?ids={','.join(delete_ids)}", timeout=10
            )
            .json()
            .get("response", {})
            .get("docs", [])
        ):
            if record["has_model_ssim"][0] == "Collection":
                delete_collections.append(record["id"])
            else:
                delete_work_ids.append(record["id"])

        try:
            n_total_works = self.solr_client.search(
                "has_model_ssim:Work", fq="ark_ssi:*", defType="lucene", rows=0
            ).hits
        except SolrError:
            n_total_works = "[unknown]"

        if len(delete_work_ids):
            if yes or click.confirm(
                f"Delete {len(delete_work_ids)} of {n_total_works} Works?"
            ):
                self.solr_client.delete(id=delete_work_ids)

        for collection_id in delete_collections:
            n_children = self.solr_client.search(
                f"member_of_collection_ids_ssim:{collection_id}",
                fq="ark_ssi:*",
                defType="lucene",
                rows=0,
            ).hits
            if yes or click.confirm(
                f"Delete collection {self.collection_names['collection_id']}? {n_children} {'child record' if n_children == 1 else 'child records'} will also be deleted."
            ):
                self.solr_client.delete(
                    q=f"id:{collection_id} OR member_of_collection_ids_ssim:{collection_id}"
                )

    def dump(self, output: TextIO = sys.stdout) -> None:
        hits = int(self.solr_client.search("*:*", rows=0).hits)
        rows = 250

        for start in range(0, hits, rows):
            results = self.solr_client.search(
                "*:*",
                start=start,
                rows=rows,
            )
            hits = results.hits
            for raw_doc in results:
                self.save_record(raw_doc, output)

    def save_record(self, record: dict[str, Any], output: TextIO = sys.stdout):
        adapter: pydantic.TypeAdapter[UrsusSolrRecord | IngestSolrRecord] = (
            pydantic.TypeAdapter(UrsusSolrRecord | IngestSolrRecord)
        )
        try:
            doc = adapter.validate_python(record)

            output.write(
                doc.model_dump_json(
                    by_alias=True,
                    exclude_none=True,
                )
                + "\n"
            )

        except pydantic.ValidationError as e:
            logging.warning(f"Could not export {record.get('id', record)}: {e}")

    def map_record(self, record: dict[str, str]) -> UrsusSolrRecord:
        collections = [
            self.collection_names[ark]
            for ark in parse_list(record.get("Parent ARK")) or []
        ]

        mapped_record = UrsusSolrRecord.model_validate(
            {
                **record,
                "member_of_collections_ssim": collections or None,
            }
        )

        if not mapped_record.thumbnail_url_ss:
            mapped_record.thumbnail_url_ss = str(
                mapped_record.access_copy_ssi
            ) or self.thumbnail_from_manifest(mapped_record)

        return mapped_record

    def thumbnail_from_manifest(self, record: UrsusSolrRecord) -> str | None:
        """Picks a thumbnail downloading the IIIF manifest.

        Args:
            record: A mapping representing the CSV record.

        Returns:
            A string containing the thumbnail URL
        """

        try:
            manifest_url = record.iiif_manifest_url_ssi
            if not isinstance(manifest_url, str):
                return None
            response = requests.get(manifest_url)
            manifest = response.json()

            canvases = {
                c["label"]: c["images"][0]["resource"]["service"]["@id"]
                for seq in manifest["sequences"]
                for c in seq["canvases"]
            }

            return (
                canvases.get("f. 001r") or list(canvases.values())[0]
            ) + "/full/!200,200/0/default.jpg"

        except Exception:
            # ruff: noqa: E722
            return None

    def collection_names_from_solr(self) -> None:
        """Get a mapping of collection IDs to collection names.

        Returns:
            A dict mapping collection IDs to collection names.
        """
        try:
            for doc in self.solr_client.search(
                "has_model_ssim:Collection",
                defType="lucene",
                fl="ark_ssi,title_tesim",
                rows=1000,
            ):
                match doc:
                    case {"ark_ssi": str(ark), "title_tesim": [title, *_]}:
                        self.collection_names[ark] = title
                    case _:
                        rich.print(f"Can't load title for collection", doc)

        except SolrError as e:
            print(f"Error querying records: {e}")

    def get_log(self):  # -> list[IngestLogRecord]:
        ingest_records = [
            IngestLogRecordReturned.model_validate(x)
            for x in self.solr_client.search("is_ingest_bsi:true").docs
        ]

        ingest_id_facets = (
            self.solr_client.search(
                "*:*",
                **{
                    "facet": "on",
                    "facet.field": "ingest_id_ssi",  # weird **{...} syntax allows "facet.field"
                    "rows": 0,
                },
            )
            .facets.get("facet_fields", {})
            .get("ingest_id_ssi", [])
        )
        ingest_counts = {}
        for i in range(0, len(ingest_id_facets), 2):
            ingest_counts[ingest_id_facets[i]] = ingest_id_facets[i + 1]

        return [
            IngestLogRecord(
                id=record.id,
                feed_ursus_version_ssi=record.feed_ursus_version_ssi,
                ingest_user_ssi=record.ingest_user_ssi,
                ingest_filenames_ssim=record.ingest_filenames_ssim,
                timestamp=record.timestamp,
                count=ingest_counts.get(record.id, 0),
            )
            for record in ingest_records
        ]

    def print_log(self):
        table = Table(title="Ingests")

        table.add_column("id")
        table.add_column("date")
        table.add_column("user")
        table.add_column("filename(s)")
        table.add_column("feed_ursus version")
        table.add_column("count")

        for row in self.get_log():
            table.add_row(
                row.id,
                row.ingest_user_ssi,
                ", ".join(row.ingest_filenames_ssim),
                row.feed_ursus_version_ssi,
                str(row.count),
            )

        console = Console()
        console.print(table)


class IngestLogRecordWrite(pydantic.BaseModel):
    """Record of a given ingest, as submitted to solr."""

    id: str
    is_ingest_bsi: Literal[True] = True
    ingest_filenames_ssim: list[str] = []
    feed_ursus_version_ssi: str
    ingest_user_ssi: str


class IngestLogRecordReturned(IngestLogRecordWrite):
    """Record of a given ingest, as returned by solr.

    Includes fields auto-generated by solr.
    """

    hashed_id_ssi: str
    _version_: int
    timestamp: datetime
    score: float


class IngestLogRecord(IngestLogRecordWrite):
    """Ingest log record, as used for reporting.

    Includes the solr-generated 'timestamp' field plus a 'count' field that must be obtained separately with a facet query on the field
    """

    timestamp: datetime
    count: int
