import type { z } from 'zod';
import type {
  GitHubCodeSearchQuerySchema,
  GitHubReposSearchSingleQuerySchema,
} from '@octocodeai/octocode-core/schemas';

type GitHubCodeSearchQuery = z.infer<typeof GitHubCodeSearchQuerySchema>;
type GitHubReposSearchSingleQuery = z.infer<
  typeof GitHubReposSearchSingleQuerySchema
>;
import type { WithOptionalMeta } from '../types/execution.js';
import { GitHubPullRequestsSearchParams } from './githubAPI.js';

export function getOwnerQualifier(owner: string): string {
  return `user:${owner}`;
}

const GITHUB_SEARCH_SPECIAL_CHARS = /[@/]/;

const FILE_PATH_TAIL = /(?:^|\/)([^/]+\.[A-Za-z][A-Za-z0-9]{0,9})$/;

const GITHUB_BARE_KEYWORD = /^[A-Za-z0-9_-]+$/;

function quoteKeywordIfNeeded(keyword: string): string {
  if (keyword.startsWith('"')) {
    return keyword;
  }
  if (!GITHUB_BARE_KEYWORD.test(keyword)) {
    return `"${keyword.replace(/"/g, '\\"')}"`;
  }
  return keyword;
}

abstract class BaseQueryBuilder {
  protected queryParts: string[] = [];

  addOwnerRepo(params: {
    owner?: string | string[] | null;
    repo?: string | string[] | null;
  }): this {
    if (params.owner && params.repo) {
      const owners = Array.isArray(params.owner)
        ? params.owner
        : [params.owner];
      const repos = Array.isArray(params.repo) ? params.repo : [params.repo];

      owners.forEach(owner => {
        repos.forEach(repo => {
          this.queryParts.push(`repo:${owner}/${repo}`);
        });
      });
    } else if (params.owner) {
      const owners = Array.isArray(params.owner)
        ? params.owner
        : [params.owner];
      owners.forEach(owner => {
        this.queryParts.push(getOwnerQualifier(owner));
      });
    }
    return this;
  }

  addDateFilters(
    params: Record<string, unknown> | GitHubPullRequestsSearchParams
  ): this {
    const dateFields: Record<string, string> = {
      created: 'created',
      updated: 'updated',
      'author-date': 'author-date',
      'committer-date': 'committer-date',
      'merged-at': 'merged',
      closed: 'closed',
    };

    Object.entries(dateFields).forEach(([paramKey, queryKey]) => {
      const value = (params as Record<string, unknown>)[paramKey];
      if (value) {
        this.queryParts.push(`${queryKey}:${value}`);
      }
    });
    return this;
  }

  addArrayFilter(
    values: string | string[] | null | undefined,
    prefix: string,
    quoted = false
  ): this {
    if (values && values !== null) {
      const valueArray = Array.isArray(values) ? values : [values];
      valueArray.forEach(value => {
        const formattedValue = quoted ? `"${value}"` : value;
        this.queryParts.push(`${prefix}:${formattedValue}`);
      });
    }
    return this;
  }

  addBooleanFilter(
    value: boolean | undefined,
    trueQuery: string,
    falseQuery: string
  ): this {
    if (value === true) {
      this.queryParts.push(trueQuery);
    } else if (value === false) {
      this.queryParts.push(falseQuery);
    }
    return this;
  }

  addSimpleFilter(
    value: string | number | null | undefined,
    key: string
  ): this {
    if (value !== undefined && value !== null) {
      this.queryParts.push(`${key}:${value}`);
    }
    return this;
  }

  addQuotedFilter(value: string | null | undefined, key: string): this {
    if (value !== undefined && value !== null) {
      const needsQuoting =
        GITHUB_SEARCH_SPECIAL_CHARS.test(value) && !value.startsWith('"');
      const formatted = needsQuoting ? `"${value}"` : value;
      this.queryParts.push(`${key}:${formatted}`);
    }
    return this;
  }

  build(): string {
    return this.queryParts.join(' ').trim();
  }
}

class CodeSearchQueryBuilder extends BaseQueryBuilder {
  addQueryTerms(params: WithOptionalMeta<GitHubCodeSearchQuery>): this {
    if (Array.isArray(params.keywords) && params.keywords.length > 0) {
      const nonEmptyTerms = params.keywords.filter(term => term && term.trim());
      if (nonEmptyTerms.length > 0) {
        this.queryParts.push(...nonEmptyTerms.map(quoteKeywordIfNeeded));
      }
    }
    return this;
  }

  addSearchFilters(params: WithOptionalMeta<GitHubCodeSearchQuery>): this {
    let path = params.path;
    let filename = params.filename;
    const fileTail =
      typeof path === 'string' && !filename ? path.match(FILE_PATH_TAIL) : null;
    if (fileTail) {
      filename = fileTail[1];
      path = path!.slice(0, fileTail.index) || undefined;
    }

    this.addSimpleFilter(filename, 'filename');
    this.addSimpleFilter(params.extension, 'extension');
    this.addQuotedFilter(path, 'path');
    if ((params as Record<string, unknown>).language) {
      this.queryParts.push(
        `language:${(params as Record<string, unknown>).language}`
      );
    }
    return this;
  }

  addMatchFilters(params: WithOptionalMeta<GitHubCodeSearchQuery>): this {
    if (params.match) {
      const matches = Array.isArray(params.match)
        ? params.match
        : [params.match];
      matches.forEach(match => {
        if (match === 'file') {
          this.queryParts.push('in:file');
        } else if (match === 'path') {
          this.queryParts.push('in:path');
        }
      });
    }
    return this;
  }
}

class RepoSearchQueryBuilder extends BaseQueryBuilder {
  addQueryTerms(params: WithOptionalMeta<GitHubReposSearchSingleQuery>): this {
    if (Array.isArray(params.keywords) && params.keywords.length > 0) {
      this.queryParts.push(...params.keywords.map(quoteKeywordIfNeeded));
    }
    return this;
  }

  addRepoFilters(params: WithOptionalMeta<GitHubReposSearchSingleQuery>): this {
    this.addArrayFilter(params.topicsToSearch, 'topic');
    this.addSimpleFilter(params.stars, 'stars');
    this.addSimpleFilter(params.size, 'size');
    this.addSimpleFilter(params.created, 'created');

    if (params.updated) {
      this.queryParts.push(`pushed:${params.updated}`);
    }
    if (params.language) {
      this.queryParts.push(`language:${params.language}`);
    }

    const p = params as Record<string, unknown>;
    if (typeof p.forks === 'string') this.queryParts.push(`forks:${p.forks}`);
    if (typeof p.license === 'string')
      this.queryParts.push(`license:${p.license}`);
    if (typeof p.goodFirstIssues === 'string')
      this.queryParts.push(`good-first-issues:${p.goodFirstIssues}`);

    return this;
  }

  addMatchFilters(
    params: WithOptionalMeta<GitHubReposSearchSingleQuery>
  ): this {
    if (params.match) {
      const matches = Array.isArray(params.match)
        ? params.match
        : [params.match];
      matches.forEach(match => {
        if (match === 'name') {
          this.queryParts.push('in:name');
        } else if (match === 'description') {
          this.queryParts.push('in:description');
        } else if (match === 'readme') {
          this.queryParts.push('in:readme');
        }
      });
    }
    return this;
  }

  addQualityFilters(
    params?: WithOptionalMeta<GitHubReposSearchSingleQuery>
  ): this {
    const p = params as Record<string, unknown> | undefined;
    const archived = p?.archived as boolean | undefined;
    this.queryParts.push(
      archived === true ? 'archived:true' : 'is:not-archived'
    );
    const visibility = p?.visibility as string | undefined;
    if (visibility === 'public') this.queryParts.push('is:public');
    else if (visibility === 'private') this.queryParts.push('is:private');
    return this;
  }
}

class PullRequestSearchQueryBuilder extends BaseQueryBuilder {
  addBasicFilters(params: GitHubPullRequestsSearchParams): this {
    if (params.query && params.query.trim()) {
      this.queryParts.push(params.query.trim());

      if (params.match && params.match.length > 0) {
        this.queryParts.push(`in:${params.match.join(',')}`);
      }
    }

    this.queryParts.push('is:pr');
    return this;
  }

  addStateFilters(params: GitHubPullRequestsSearchParams): this {
    // `is:merged` already implies a closed PR. The caller maps state:"merged"
    // to state:"closed" + merged:true (needed for the REST path), but in the
    // search query that yields a redundant, over-constrained `is:closed
    // is:merged` — emit only `is:merged`.
    const redundantClosed = params.merged === true && params.state === 'closed';
    if (!redundantClosed) {
      this.addSimpleFilter(params.state, 'is');
    }
    this.addBooleanFilter(params.draft, 'is:draft', '-is:draft');
    this.addBooleanFilter(params.merged, 'is:merged', 'is:unmerged');
    return this;
  }

  addUserFilters(params: GitHubPullRequestsSearchParams): this {
    this.addSimpleFilter(params.author, 'author');
    this.addSimpleFilter(params.assignee, 'assignee');
    this.addSimpleFilter(params.mentions, 'mentions');
    this.addSimpleFilter(params.commenter, 'commenter');
    this.addSimpleFilter(params.involves, 'involves');
    this.addSimpleFilter(params['reviewed-by'], 'reviewed-by');
    this.addSimpleFilter(params['review-requested'], 'review-requested');
    return this;
  }

  addBranchFilters(params: GitHubPullRequestsSearchParams): this {
    this.addSimpleFilter(params.head, 'head');
    this.addSimpleFilter(params.base, 'base');
    return this;
  }

  addEngagementFilters(params: GitHubPullRequestsSearchParams): this {
    this.addSimpleFilter(params.comments, 'comments');
    this.addSimpleFilter(params.reactions, 'reactions');
    this.addSimpleFilter(params.interactions, 'interactions');
    return this;
  }

  addReviewFilters(params: GitHubPullRequestsSearchParams): this {
    if (params.review) this.queryParts.push(`review:${params.review}`);
    return this;
  }

  addOrganizationFilters(params: GitHubPullRequestsSearchParams): this {
    this.addArrayFilter(params.label, 'label', true);
    if (params.milestone)
      this.queryParts.push(`milestone:"${params.milestone}"`);
    return this;
  }

  addNegativeFilters(params: GitHubPullRequestsSearchParams): this {
    if (params['no-assignee']) this.queryParts.push('no:assignee');
    if (params['no-label']) this.queryParts.push('no:label');
    if (params['no-milestone']) this.queryParts.push('no:milestone');
    if (params['no-project']) this.queryParts.push('no:project');
    if (params.locked === true) this.queryParts.push('is:locked');
    else if (params.locked === false) this.queryParts.push('is:unlocked');
    if (params.visibility === 'public') this.queryParts.push('is:public');
    else if (params.visibility === 'private')
      this.queryParts.push('is:private');
    if (params['team-mentions'])
      this.queryParts.push(`team:${params['team-mentions']}`);
    if (params.project) this.queryParts.push(`project:${params.project}`);
    return this;
  }

  addMiscFilters(params: GitHubPullRequestsSearchParams): this {
    this.queryParts.push(
      params.archived === true ? 'archived:true' : 'archived:false'
    );
    if (params.language) this.queryParts.push(`language:${params.language}`);
    if (params.checks) this.queryParts.push(`status:${params.checks}`);
    return this;
  }
}

export function buildCodeSearchQuery(
  params: WithOptionalMeta<GitHubCodeSearchQuery>
): string {
  return new CodeSearchQueryBuilder()
    .addQueryTerms(params)
    .addSearchFilters(params)
    .addOwnerRepo(params)
    .addMatchFilters(params)
    .build();
}

export function buildRepoSearchQuery(
  params: WithOptionalMeta<GitHubReposSearchSingleQuery>
): string {
  return new RepoSearchQueryBuilder()
    .addQueryTerms(params)
    .addOwnerRepo(params)
    .addRepoFilters(params)
    .addMatchFilters(params)
    .addQualityFilters(params)
    .build();
}

export function buildPullRequestSearchQuery(
  params: GitHubPullRequestsSearchParams
): string {
  return new PullRequestSearchQueryBuilder()
    .addBasicFilters(params)
    .addOwnerRepo(params)
    .addStateFilters(params)
    .addUserFilters(params)
    .addBranchFilters(params)
    .addDateFilters(params)
    .addEngagementFilters(params)
    .addReviewFilters(params)
    .addOrganizationFilters(params)
    .addNegativeFilters(params)
    .addMiscFilters(params)
    .build();
}

export function shouldUseSearchForPRs(
  params: GitHubPullRequestsSearchParams
): boolean {
  return (
    params.draft !== undefined ||
    params.author !== undefined ||
    params.assignee !== undefined ||
    (typeof params.query === 'string' && params.query.trim().length > 0) ||
    (params.label && params.label.length > 0) ||
    params.mentions !== undefined ||
    params.commenter !== undefined ||
    params.involves !== undefined ||
    params['reviewed-by'] !== undefined ||
    params['review-requested'] !== undefined ||
    params.reactions !== undefined ||
    params.comments !== undefined ||
    params.interactions !== undefined ||
    params['no-assignee'] !== undefined ||
    params['no-label'] !== undefined ||
    params['no-milestone'] !== undefined ||
    params['no-project'] !== undefined ||
    params.state === 'merged' ||
    params.milestone !== undefined ||
    params.language !== undefined ||
    params.checks !== undefined ||
    params.review !== undefined ||
    params.locked !== undefined ||
    params.visibility !== undefined ||
    params['team-mentions'] !== undefined ||
    params.project !== undefined ||
    params.created !== undefined ||
    params.updated !== undefined ||
    params['merged-at'] !== undefined ||
    params.closed !== undefined ||
    params.merged !== undefined ||
    (params.match !== undefined && params.match.length > 0) ||
    params.sort === 'comments' ||
    params.sort === 'reactions' ||
    Array.isArray(params.owner) ||
    Array.isArray(params.repo)
  );
}
