from typing import Optional
import itertools
import logging
import json
import hashlib
import datetime
import html
from collections import defaultdict

from startrek_client import Startrek
from startrek_client.exceptions import StartrekError

from stackbot.config import settings
from stackbot.db import dbconnect, Subscription, Email, BotUser
from stackbot.logic.clients.stack import StackClient
from stackbot import enums


logger = logging.getLogger(__name__)


COMMENT_SCORES = defaultdict(dict)
TAG_EXPERTS = defaultdict(set)


def get_unique_from_post(post: dict):
    return hashlib.sha256(''.join([post['link'], settings.STARTREK_PEPPER]).encode()).hexdigest()


def tags_subject_matter_expert(post: dict, login: str) -> list[str]:
    return list(TAG_EXPERTS[login].intersection(post['tags']))


def create_issue_from_post(client: Startrek, post: dict, original_post: Optional[dict] = None):
    components = []
    post_type_noun = ''
    if post['post_type'] == 'article':
        components = [settings.COMPONENT_ID_QUESTION]
        post_type_noun = 'статьи'
    elif post['post_type'] == 'question':
        components = [settings.COMPONENT_ID_QUESTION]
        post_type_noun = 'вопроса'
    elif post['post_type'] == 'answer':
        components = [settings.COMPONENT_ID_ANSWER]
        post_type_noun = 'комментария'

    login = get_owner_login(post)
    issue_body = post['body_markdown']
    issue_body += '\n---\nАвтор {} @{}'.format(post_type_noun, login)
    if post['post_type'] == 'answer' and original_post:
        tags_expert = tags_subject_matter_expert(post=original_post, login=login)
        if tags_expert:
            issue_body += ' — subject matter expert для тег{} {}'.format(
                'a' if len(tags_expert) == 1 else 'ов', ', '.join(tags_expert)
            )

    upvotes_count = post['up_vote_count'] if 'up_vote_count' in post else 0
    return client.issues.create(
        queue=settings.STARTREK_QUEUE,
        summary=html.unescape(post['title']),
        type={'id': 66},
        description=issue_body,
        tags=post['tags'],
        components=components,
        unique=get_unique_from_post(post),
        **{settings.SO_UPVOTES_ID: upvotes_count},
    )


def update_issue_with_comments(post: dict, issue, original_post: Optional[dict] = None) -> None:
    if not post['comment_count']:
        post['comments'] = []

    current_comments = list(issue.comments.get_all())
    current_idx = 0
    for comment in post['comments']:
        owner_login = get_owner_login(comment)
        comment_body = comment['body_markdown']
        comment_body += '\n---\nАвтор комментария @{}'.format(owner_login)

        target_post = original_post or post
        tags_expert = tags_subject_matter_expert(post=target_post, login=owner_login)
        if tags_expert:
            comment_body += ' — subject matter expert для тег{} {}'.format(
                'a' if len(tags_expert) == 1 else 'ов', ', '.join(tags_expert)
            )

        if current_comments and current_idx < len(current_comments):
            if comment_body != current_comments[current_idx].text:
                logger.warning(
                    'Comment text in SO and in ST differ: %s %s %d', post['link'], issue.key, current_idx
                )
            current_idx += 1
        else:
            comment_id = issue.comments.create(text=comment_body)['id']
            COMMENT_SCORES[issue.key][comment_id] = comment['score']


def get_owner_login(post: dict) -> str:
    return post['owner']['display_name']


def close_issue(issue):
    if 'close' in set({transition.id for transition in list(issue.transitions.get_all())}):
        issue.transitions['close'].execute(resolution='fixed')


def create_issue_link(issue, linked_issue, relationship: str):
    current_links = list(issue.links.get_all())
    if not current_links:
        issue.links.create(issue=linked_issue.key, relationship=relationship)
    elif linked_issue.key not in {link.object.key for link in current_links}:
        issue.links.create(issue=linked_issue.key, relationship=relationship)


def process_question_answers(client: Startrek, question_issue, post: dict) -> bool:
    answers = post['answers']
    has_accepted_answer = False
    for answer in answers:
        answer['post_type'] = 'answer'
        relationship = 'depends on' if answer['is_accepted'] else 'relates'

        has_accepted_answer |= answer['is_accepted']

        answer_issue = create_issue_from_post(client=client, post=answer, original_post=post)
        update_issue_with_comments(post=answer, issue=answer_issue, original_post=post)
        create_issue_link(issue=question_issue, linked_issue=answer_issue, relationship=relationship)
        close_issue(issue=answer_issue)

    return has_accepted_answer


def process_post(client: Startrek, post: dict) -> str:
    logger.info('Processing post: %s', post['link'])

    if 'article_id' in post:
        post['post_type'] = 'article'
    elif 'question_id' in post:
        post['post_type'] = 'question'

    try:
        issue = create_issue_from_post(client=client, post=post)
        update_issue_with_comments(post=post, issue=issue)

        has_accepted_answer = False
        if post['post_type'] == 'question' and post['answer_count']:
            has_accepted_answer = process_question_answers(
                client=client, question_issue=issue, post=post
            )
        if post['post_type'] == 'article' or has_accepted_answer:
            close_issue(issue=issue)

        return issue.key

    except StartrekError as err:
        logger.error('Error while processing post %s: %s', post['link'], err)


def prepare_data(filename: Optional[str] = None) -> tuple[list[dict], list[dict]]:
    if not filename:
        stack = StackClient()
        return stack.all_posts()
    with open(filename, 'r', encoding='utf-8') as file:
        data = json.load(file)

    with open('SME_mapping.json', 'r', encoding='utf-8') as file:
        sme_data = json.load(file)
    with open('tagSynonyms.json', 'r', encoding='utf-8') as file:
        tag_synonyms_data = json.load(file)

    tag_synonyms = defaultdict(list)
    for item in tag_synonyms_data['rows']:
        tag_synonyms[item['SourceTagName']].append(item['TargetTagName'])
        tag_synonyms[item['TargetTagName']].append(item['SourceTagName'])

    for item in sme_data['rows']:
        login = item['email'].split('@')[0]
        TAG_EXPERTS[login].add(item['tag'])
        for synonym in tag_synonyms[item['tag']]:
            TAG_EXPERTS[login].add(synonym)

    return data[0], data[1]


def so_migrate(
    only_prepare_data: Optional[bool] = False,
    use_prepared_data: Optional[bool] = False,
    prepared_data_filename: Optional[str] = 'prepared_data.json',
    processed_list_filename: Optional[str] = 'processed_posts.json',
    process_only_previously_failed_posts: Optional[bool] = False,
):
    timestamp = int(datetime.datetime.now().timestamp())
    logger.info('Start SO migration to ST, queue: %s, timestamp: %d', settings.STARTREK_QUEUE, timestamp)

    articles, questions = prepare_data(prepared_data_filename if use_prepared_data else None)
    startrek = Startrek(
        useragent='python',
        base_url=settings.STARTREK_API_HOST,
        token=settings.STARTREK_API_TOKEN,
    )

    posts = itertools.chain(articles, questions)
    processed = {}
    if process_only_previously_failed_posts:
        with open(processed_list_filename, 'r', encoding='utf-8') as file:
            processed = json.load(file)
        posts = [post for post in posts if post['link'] not in processed]

    try:
        if only_prepare_data:
            with open(f'prepared_data_{timestamp}.json', 'w', encoding='utf-8') as file:
                json.dump([articles, questions], file, ensure_ascii=False)
            with open(f'tag_experts_{timestamp}.json', 'w', encoding='utf-8') as file:
                json.dump({k: list(v) for k, v in TAG_EXPERTS.items()}, file, ensure_ascii=False)
            return

        for post in posts:
            issue_key = process_post(client=startrek, post=post)
            if issue_key:
                processed[post['link']] = issue_key
    finally:
        with open(f'processed_result_{timestamp}.json', 'w', encoding='utf-8') as file:
            json.dump(processed, file, ensure_ascii=False)
        with open(f'comment_scores_{timestamp}.json', 'w', encoding='utf-8') as file:
            json.dump(dict(COMMENT_SCORES), file, ensure_ascii=False)

        logger.info(
            'Finish SO migration, %s items processed out of %s',
            settings.STARTREK_QUEUE, len(processed), len(articles) + len(questions)
        )


@dbconnect
def import_tag_subs(session, tag_subs_filename: Optional[str] = 'tags_subs.json'):
    with open(tag_subs_filename, 'r', encoding='utf-8') as file:
        tag_subs = json.load(file)['rows']

    robot_user = session.query(BotUser).filter(BotUser.staff_login == settings.ROBOT_LOGIN).first()
    if not robot_user:
        robot_user = BotUser(
            staff_id=366975,
            staff_uid='1120000000517630',
            staff_login=settings.ROBOT_LOGIN,
            state=enums.BotUserState.active,
        )
        session.add(robot_user)
        session.commit()

    existing_subs = defaultdict(set)
    for sub in session.query(Subscription).filter(Subscription.type == enums.SubscriptionType.email):
        login = sub.email.author.staff_login
        if login == settings.ROBOT_LOGIN:
            real_login = sub.email.address.split('@')[0]
            existing_subs[real_login].add(sub.tag)
        else:
            existing_subs[login].add(sub.tag)

    for sub in tag_subs:
        if sub['Name'] == 'None':
            continue

        tag = sub['TagName']
        login = sub['email'].split('@')[0]
        if login in existing_subs and tag in existing_subs[login]:
            continue

        email = session.query(Email).filter(Email.address == sub['email']).first()
        if not email:
            user = session.query(BotUser).filter(BotUser.staff_login == login).first()
            email = Email(address=sub['email'], author=user or robot_user)
            session.add(email)

        session.add(Subscription(
            tag=tag, email=email, type=enums.SubscriptionType.email,
        ))


def find_imported_subs(email: str, session) -> list[Subscription]:
    return session.query(Subscription).join(
        Email, Email.id == Subscription.email_id
    ).join(
        BotUser, BotUser.id == Email.author_id
    ).filter(
        BotUser.staff_login == settings.ROBOT_LOGIN,
        Email.address == email
    ).all()


def take_ownership(subs: list[Subscription], bot_user: BotUser) -> None:
    emails = {sub.email for sub in subs}
    for email in emails:
        email.author = bot_user
