"""
Assumptions:

- The filenames contain names but the library, as a whole cluster around 
  specific individuals, with very little overlap (though they certainly exist).
  So, for example, if we encounter "Emily Browning" subsequent references to
  "Emily" is probably also "Emily Browning" but "Emily Blunt" is probably also
  in there.
- Names are weird, the filenames even weirder/less standard.
"""
from argparse import ArgumentParser
from collections import OrderedDict
from enum import Enum
from importlib import import_module
from typing import Iterable, Optional, List, Set, Tuple, Union

import logging
import os
import re
import sqlite3

NONWORD: re.Pattern = re.compile("\W+")
logger = logging.getLogger(__file__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())

class NameDecisionRule(Enum):
    ALMOST_CERTAIN = "almost-certain"
    TRUNCATED_FIRSTNAME = "truncated-firstname"
    LASTNAME_BACKWARD = "lastname-backward"
    MANUAL_INPUT = "manual-input"

NameTuple = Tuple[str, Optional[str], NameDecisionRule]

class Indexerdem(object):

    SQLITE_TRUE = 1
    SQLITE_FALSE = 0

    def __init__(self, index_filename: str, locales: Iterable[str], extensions: Iterable[str]):
        self.conn = sqlite3.connect(index_filename)
        self.first_names_female: Set[str] = set()
        self.last_names: Set[str] = set()
        self.extensions: Set[str] = set(extensions)
        for loc in locales:
            person_providers_module = import_module("faker.providers.person.%s" % loc)
            self.first_names_female |= self.__extract_names(person_providers_module.Provider.first_names_female) # type: ignore
            self.last_names |= self.__extract_names(person_providers_module.Provider.last_names) # type: ignore
            self.last_names |= self.__extract_names(person_providers_module.Provider.first_names_male) # type: ignore

    def __extract_names(self, faker_listings: Union[OrderedDict, Tuple]) -> Set[str]:
        if isinstance(faker_listings, tuple):
            return set(faker_listings)
        elif isinstance(faker_listings, OrderedDict):
            return set(faker_listings.keys())
        else:
            raise TypeError("We can only operate with OrderedDicts or tuples. Given: %s" % type(faker_listings))

    def init(self):
        cursor = self.conn.cursor()

        cursor.execute("PRAGMA foreign_keys=ON")
        cursor.execute("""CREATE TABLE IF NOT EXISTS files
                          (id INTEGER PRIMARY KEY ASC,
                           filename TEXT UNIQUE NOT NULL,
                           fullpath TEXT NOT NULL);""")
        cursor.execute("""CREATE TABLE IF NOT EXISTS persons
                          (id INTEGER PRIMARY KEY ASC,
                           firstname TEXT NOT NULL,
                           lastname TEXT,
                           extraction_rule TEXT NOT NULL,
                           UNIQUE(firstname, lastname));""")
        cursor.execute("""CREATE TABLE IF NOT EXISTS participation
                          (person_id INTEGER, file_id INTEGER, is_certain INTEGER NOT NULL DEFAULT {is_certain_default},
                           FOREIGN KEY(person_id) REFERENCES persons(id),
                           FOREIGN KEY(file_id) REFERENCES files(id),
                           UNIQUE(person_id, file_id))""".format(is_certain_default=Indexerdem.SQLITE_TRUE))
        self.conn.commit()

    def __normalize_filename(self, filename: str) -> str:
        spam = filename.rsplit(".", 1)
        return spam[0].replace("&", "and")

    def __find_names(self, haystack: str) -> Iterable[NameTuple]:
        hayparse: List[str] = NONWORD.split(haystack)
        names: List[NameTuple] = []
        i = 0
        limit = len(hayparse)

        while i < limit:
            word = hayparse[i].title()

            # FIXME This assumes names are only two-part. The "reasonable" thing
            # to do would be to consume until we encounter something that isn't
            # a name. However, watch out that sanitation means we lose boundary
            # cues in comma-listed names.
            if word in self.first_names_female:
                forward = i + 1
                if forward < limit:
                    if hayparse[forward] in self.last_names:
                        names.append((word, hayparse[forward], NameDecisionRule.ALMOST_CERTAIN))
                        i = forward + 1
                        continue
                    else:
                        names.append((word, None, NameDecisionRule.TRUNCATED_FIRSTNAME))
                else:
                    names.append((word, None, NameDecisionRule.TRUNCATED_FIRSTNAME))
            elif word in self.last_names:
                backward = i - 1
                if backward >= 0:
                   names.append((hayparse[backward], word, NameDecisionRule.LASTNAME_BACKWARD))


            i += 1
        
        return names

    def __get_person_id(self, cursor, firstname: str, lastname: Optional[str]) -> Optional[int]:
        if lastname is not None:
            test = cursor.execute("SELECT id FROM persons WHERE firstname=? AND lastname=? LIMIT 1;", (firstname, lastname)).fetchone()
        else:
            test = cursor.execute("SELECT id FROM persons WHERE firstname=? AND lastname IS NULL LIMIT 1;", (firstname,)).fetchone()

        if test:
            return test[0]
        else:
            return None

    def index(self, filename: str, fullpath: str) -> None:
        def decide_certainty(nametpl: NameTuple) -> bool:
            return nametpl[2] == NameDecisionRule.ALMOST_CERTAIN

        try:
            cleaned = self.__normalize_filename(filename)
            cursor = self.conn.cursor()
            cursor.execute("INSERT INTO files (filename, fullpath) VALUES (?, ?)", (filename, fullpath))
            file_id = cursor.lastrowid
            names = self.__find_names(cleaned)
            logger.info("'%s' has the ff. names: %s" % (filename, names))

            for name in names:
                if len(name) == 3:
                    person_id: Optional[int] = self.__get_person_id(cursor, name[0], name[1])
                    if person_id is None:
                        person = (name[0], name[1], name[2].value)
                        cursor.execute("INSERT INTO persons (firstname, lastname, extraction_rule) VALUES (?, ?, ?)", person)
                        person_id = cursor.lastrowid

                    certainty = Indexerdem.SQLITE_TRUE if decide_certainty(name) else Indexerdem.SQLITE_FALSE
                    cursor.execute(
                        "INSERT INTO participation (person_id, file_id, is_certain) VALUES (?, ?, ?);",
                        (person_id, file_id, certainty)
                    )
                else:
                    logger.error("Found an odd name: %s" % str(name))

        except:
            logger.exception("Ran into some problems...")
        finally:
            self.conn.commit()

    def __get_ext(self, fname: str) -> str:
        return fname.rsplit(".", 1)[1]

    def readdir(self, dirpath: str) -> None:
        try:
            for root, dirs, files in os.walk(dirpath):
                for _file in files:
                    if self.__get_ext(_file) in self.extensions:
                        logger.info("processing %s" % _file)
                        self.index(_file, root)
        except:
            logger.exception("Ran into some problems...")
        finally:
            self.conn.close()

if __name__ == "__main__":
    parser = ArgumentParser(description="indexer for erdem.")
    parser.add_argument(
        "--filepath", "-f", required=True, type=str,
        help="The full filepath of the directory to index."
    )
    parser.add_argument(
        "--locales", "-l", type=str, default="en,en_GB,en_US,en_NZ",
        help="Comma-separated list of locales that we will use to detect names."
    )
    args = vars(parser.parse_args())
    indexer: Indexerdem = Indexerdem("cache.db", args["locales"].split(","), ("mp4", "avi", "flv", "mkv"))
    indexer.init()
    indexer.readdir(args["filepath"])
