#!/usr/bin/env python3

import argparse
import inspect
import json
import logging
import os
import time
from datetime import datetime
from typing import Any

import pandas as pd
import requests
import yaml
from markitdown import MarkItDown
from tqdm import tqdm

from arkeo.cfg import ConfigurationManager
from arkeo.clean import MarkdownCleaner, extract_yaml_header
from arkeo.extract import Extractor
from arkeo.ingest import Ingester, EXT_TEXT
from arkeo.keys import PrimaryKey, format_nav_path, is_nav_key_format
from arkeo.path import PathManager
from ref.agency import AgencyLoader
from ref.caligula import Caligula
from ref.geonames import GeonamesManager, CITIES, STATES, CITY_STATE
from ref.provider import ProviderManager
from utils.html import get_headers_by_index
from utils.init import download_nltk_data, setup_logging, setup_slogging
from utils.json import load_json
from utils.schema import SchemaTransformer
from utils.url import is_valid_url


setup_logging(level=logging.INFO)
log = logging.getLogger(__name__)


def smart_instantiate(class_name: str, available_args: dict) -> Any:
    cls = globals()[class_name]
    sig = inspect.signature(cls.__init__)

    # build kwargs from available arguments
    kwargs = {}
    for param_name, param in sig.parameters.items():
        if param_name == "self":
            continue

        if param_name in available_args:
            kwargs[param_name] = available_args[param_name]
        elif param.default == inspect.Parameter.empty:
            raise ValueError(f"missing required parameter: {param_name}")

    return cls(**kwargs)


def main(argv: list[str] = None) -> None:
    t0 = time.time()

    parser = argparse.ArgumentParser()
    parser.description = "...from around the corner, out of sight"
    parser.add_argument("method", nargs="?", help="method for your madness")
    parser.add_argument("params", nargs="*", help="eg, attributes to index")

    args = parser.parse_args(argv)
    if args.method:
        path_manager = PathManager()
        setup_slogging(path=path_manager.temp, drive_manager=path_manager.drive)

        if is_valid_url(args.method) or is_nav_key_format(args.method):
            download_nltk_data()
            Ingester(path_manager).process(args.method)

        if "." in args.method and args.method.count(".") == 1:
            # class.method called directly
            class_name, method_name = args.method.split(".", 1)

            arg_name_dm = "path_manager"
            cls = smart_instantiate(class_name, {arg_name_dm: path_manager})
            result = getattr(cls, method_name)(args.params)

            log.info(f"{class_name}.{method_name}({arg_name_dm}) = {result}")

        else:
            # shortcut
            class_name = None
            method_name = args.method

        # update corpus
        if method_name == "ingest":
            if args.params:
                download_nltk_data()
                Ingester(path_manager).process(args.params[0])

        elif method_name == "remark":
            if args.params:
                # Ingester(path_manager).reprocess_markdown(args.params[0])
                MarkdownCleaner(path_manager).reprocess(args.params[0])

        elif method_name == "bulk":
            filename = f"{path_manager.temp}urls.{EXT_TEXT}"

            inge = Ingester(path_manager)
            inge.process(filename)

            timestamp = datetime.now().strftime("%y%m%d%H%M")
            arc_path = filename.replace(f"{EXT_TEXT}", f"{timestamp}.{EXT_TEXT}")
            path_manager.drive.rename(filename, arc_path)

        # build index
        elif method_name == "extract":
            if args.params:
                Extractor(path_manager).process(args.params[0])

        # reference
        elif method_name == "agency":
            if args.params:
                command = args.params[0]
                args.params.remove(command)
                AgencyLoader(path_manager).run(command, args.params)

        elif method_name == "cfgs":
            cm = ConfigurationManager(path_manager)
            cm.sort()

        elif method_name == "satan":
            if args.params:
                filename = args.param[0]  # "fedreg_250703.txt"
                satan = Caligula(path_manager)
                hell = satan.query_federal_register(satan.dir_data + filename)
                log.warning(hell)

        elif method_name == "fatso":  # bulk load into corpus
            if args.params:
                download_nltk_data()

                satan = Caligula(path_manager)
                raw_file = args.params[0]  # 'fedreg_250703.txt'
                inge = Ingester(path_manager)

                eos = path_manager.drive.read_or_none(satan.dir_data + raw_file)
                for eo in eos.split("\n")[2:]:
                    inge.process(eo)

        elif method_name.startswith("geo"):
            gm = GeonamesManager(path_manager)
            if method_name == "geotag":
                if args.params:
                    temp_path = gm.path.temp + args.params[0]
                    target = gm.drive.read_or_none(temp_path)
                    if target:
                        found = gm.find(target, args.params[1])
                        log.info(f"test: {found}")
                    else:
                        geoname = args.params[0]
                        if geoname == STATES:  # only states identifed
                            keywords = gm.get_keywords_states(args.params[1])
                        else:
                            keywords = gm.get_keywords_cities(args.params[0])
                        log.info(
                            f"keywords, curated: {keywords}; not: {list(gm.get(CITY_STATE).keys())}."
                        )

            # system methods
            elif method_name == "geonames":
                filename = "cities.250706.psv"
                log.info(gm._extract_dsv_from_geonames(reference_file=filename))
            elif method_name == "geoall":
                log.info(gm.overwrite_all())
            elif method_name == "geostates":
                log.info(gm.overwrite_from_dsv(STATES))
            elif method_name == "geocities":
                log.info(gm.overwrite_from_dsv(CITIES))
            else:
                if args.params:
                    log.info(gm.get(args.params[0]))

        elif method_name == "pub":
            pm = ProviderManager(path_manager)
            if not args.params:
                pm.apply_delta()
            elif args[0] == "dsv":
                pm._overwrite_from_dsv()
            else:
                domain = args.params[0]
                log.info(pm.get_name(domain))
                log.info(pm.get_category(domain))

        # utilities
        elif method_name == "schema":
            if args.params:
                st = SchemaTransformer(path_manager.drive)
                dir_path = path_manager.reference

                scope = args.params[0]
                if scope.startswith("geo"):
                    dir_path += "geonames/"  # GM.dir_data
                    if scope == "geostates":  # transform psv -> json
                        # DUPLICATES GM._overwrite_from_dsv except live load
                        filename = f"{dir_path}states.psv"
                        schema_path = f"{dir_path}geostate-schema.json"
                        st.process_file(filename, schema_path)
                    elif scope == "geocities":  # transform psv -> json
                        # DUPLICATES GM._overwrite_from_dsv except live load
                        filename = f"{dir_path}cities.psv"
                        schema_path = f"{dir_path}geocity-schema.json"
                        st.process_file(filename, schema_path)
                    else:  # ie, geocity, geostate ~ SINGULAR
                        schema_path = f"{dir_path}{scope}-schema.json"
                        schema = load_json(path_manager.drive, schema_path)
                        prefix = bool(args.params[1]) if len(args.params) > 1 else False
                        headers = st.extract_field_names(schema, prefix)
                        log.info(f"{scope}: {headers}")

        elif method_name == "markitdown":
            # TODO move to its own thing + pk ~> research: replace newspaper3k except metadata
            if args.params:
                if False:
                    # non-html saved raw associated with nav_key
                    dir_nav = format_nav_path(nav_key=args.params[0])
                    raw_file = args.params[1]
                    raw_path = f"{path_manager.raw }{dir_nav}_{raw_file}"
                    md_path = f"{path_manager.corpus}{dir_nav}.md"
                else:
                    filename = args.params[0]
                    raw_path = f"{path_manager.temp}{filename}.pdf"
                    md_path = f"{path_manager.temp}{filename}.md"

                md = MarkItDown()
                mdo = md.convert(raw_path)
                path_manager.drive.write(md_path, mdo.text_content)

        # alpha
        elif method_name == "pk":
            if args.params:
                pk = args.params[0]
                log.info(
                    f"nav-key {PrimaryKey(path_manager).get_nav_key(pk)} from pk {pk}"
                )

        elif method_name == "damnit":  # regex
            mdc = json.loads(
                '{"exclude":[{"find":"([^U]\\\\.)([A-Za-z])","repl":"\\\\1\\\\n\\\\n\\\\2"}]}'
            )
            catfruit = MarkdownCleaner(path_manager)._apply_markdown_edits(
                "The cat.Apple", mdc
            )
            log.info(catfruit)

        elif method_name == "timeline":
            pk = PrimaryKey(path_manager)
            timeline_items = []
            for _, uc_data in pk.pks.items():
                nav_key = uc_data.get("nav_key", "")
                url = uc_data.get("url", "")
                if "federalregister.gov" in url:
                    file_path = f"{pk.path.corpus}{format_nav_path(nav_key)}.md"
                    if not pk.drive.file_exists(file_path):
                        log.warning(f"{nav_key} not found")
                    else:
                        log.info(f"{nav_key} found")
                        markdown = pk.drive.read_or_none(file_path)
                        yaml_header, markdown = extract_yaml_header(markdown)
                        yaml_data = yaml.safe_load(yaml_header)

                        # {id: 1, content: 'Project Start', start: '2023-01-01', title: 'Kickoff!'}
                        url = yaml_data.get("canonical", "")
                        ieo = url.find("2025-")
                        eo = url[ieo : ieo + 10]
                        date = yaml_data.get("date", "")
                        title = yaml_data.get("title", "")

                        timeline_item = {}
                        timeline_item["id"] = eo
                        timeline_item["content"] = (
                            f'<a href="{url}">{eo[5:]} ({date})</a>'
                        )
                        timeline_item["start"] = date + "T00:00:00Z"
                        timeline_item["title"] = title
                        timeline_items.append(timeline_item)
                        log.info(f"{timeline_item}")
            if timeline_items:
                file_path = f"{pk.path.temp}timeline.json"
                pk.drive.write_or_false(file_path, json.dumps(timeline_items))

        elif method_name == "reconcile":
            ucs = {}
            pk = PrimaryKey(path_manager)
            for key, data in pk.pks.items():
                url = data["url"]
                nav_key = data["nav_key"]
                if url in ucs:
                    log.warning(f"{key} vs existing {ucs[url]} : {url}")
                else:
                    ucs[url] = nav_key

            filename = f"{path_manager.temp}reconcile.txt"
            with open(filename, "r+") as f:
                lines = f.readlines()

                for i, line in enumerate(lines):
                    if len(line) < 25:  # Skip lines that are too short
                        continue

                    url = line[25:].strip()
                    if url in ucs:
                        # Calculate position and update first character
                        line_start = sum(len(lines[j]) for j in range(i))

                        if line[0] == "E":
                            log.info(f"{ucs[url]}: {url}")
                            f.seek(line_start)
                            f.write("X")

                        f.seek(line_start + 13)
                        f.write(ucs[url])  # Write first 3 chars of the key
                    time.sleep(1)

    else:
        path_manager = PathManager()
        setup_slogging(path=path_manager.temp, drive_manager=path_manager.drive)

        """
    current workspace
    
    TODO: constants to aliased dataclass
    TODO: utils.text other classes to individual py
    TODO: geonames.GeonamesManager IX to file, other types outside class
    TODO: PublisherManger.overwrite_from_dsv -> utils.json.SchemaTransformer
        """

        if True:  # workspace for other check
            nav_key = "2025-08-6EI"
            if False:
                download_nltk_data()
                Ingester(path_manager).process(nav_key)
            else:
                MarkdownCleaner(path_manager).reprocess(nav_key)

    tt = time.time()
    log.info(f"timer: {tt-t0:.2f} s")


if __name__ == "__main__":
    main()  # use []!
